diff --git a/.github/workflows/trigger-package-and-publish.yml b/.github/workflows/trigger-package-and-publish.yml new file mode 100644 index 000000000000..c7244f3b3e1d --- /dev/null +++ b/.github/workflows/trigger-package-and-publish.yml @@ -0,0 +1,93 @@ +name: Trigger Package and Publish + +on: + push: + branches: + - "att/**" + workflow_dispatch: + inputs: + trigger-type: + description: "How to trigger the target workflow" + required: true + type: choice + options: + - api + - workflow-run + +jobs: + trigger: + runs-on: flexcloud-rocky + env: + target_owner: "${{ secrets.TARGET_OWNER }}" + target_name: "${{ secrets.TARGET_NAME }}" + target_workflow: "${{ secrets.TARGET_WORKFLOW }}" + event_type: "${{ secrets.TARGET_EVENT_TYPE }}" + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + + - name: Install DNF Config Manager + run: | + sudo dnf -y install 'dnf-command(config-manager)' + + - name: Install GitHub CLI + run: | + sudo dnf -y config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo + sudo dnf -y install gh + + - name: Install gettext (for envsubvst) + run: | + sudo dnf -y install gettext + + - name: Install YQ + run: | + sudo curl -L -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod a+x /usr/local/bin/yq + + - name: Authenticate GitHub CLI Using Access Token + run: | + echo "${{ secrets.TARGET_REPOSITORY_ACCESS_TOKEN }}" | gh auth login --with-token + gh auth status + + # Triggering workflows via the API is meant to be used by external systems. + - name: Trigger Workflow in Target Repository (api) + if: "${{ github.event.inputs.trigger-type == 'api'}}" + run: | + event_data=$(cat < { int expungeJobsForSchedules(List scheduleId, Date dateAfter); int expungeJobsBefore(Date currentTimestamp); + + VMScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduledJobDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduledJobDaoImpl.java index 50a2b12fd774..2f08a41b92e4 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduledJobDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduledJobDaoImpl.java @@ -39,6 +39,8 @@ public class VMScheduledJobDaoImpl extends GenericDaoBase expungeJobForScheduleSearch; + private final SearchBuilder scheduleAndTimestampSearch; + static final String SCHEDULED_TIMESTAMP = "scheduled_timestamp"; static final String VM_SCHEDULE_ID = "vm_schedule_id"; @@ -58,6 +60,11 @@ public VMScheduledJobDaoImpl() { expungeJobForScheduleSearch.and(VM_SCHEDULE_ID, expungeJobForScheduleSearch.entity().getVmScheduleId(), SearchCriteria.Op.IN); expungeJobForScheduleSearch.and(SCHEDULED_TIMESTAMP, expungeJobForScheduleSearch.entity().getScheduledTime(), SearchCriteria.Op.GTEQ); expungeJobForScheduleSearch.done(); + + scheduleAndTimestampSearch = createSearchBuilder(); + scheduleAndTimestampSearch.and(VM_SCHEDULE_ID, scheduleAndTimestampSearch.entity().getVmScheduleId(), SearchCriteria.Op.EQ); + scheduleAndTimestampSearch.and(SCHEDULED_TIMESTAMP, scheduleAndTimestampSearch.entity().getScheduledTime(), SearchCriteria.Op.EQ); + scheduleAndTimestampSearch.done(); } /** @@ -92,4 +99,12 @@ public int expungeJobsBefore(Date date) { sc.setParameters(SCHEDULED_TIMESTAMP, date); return expunge(sc); } + + @Override + public VMScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp) { + SearchCriteria sc = scheduleAndTimestampSearch.create(); + sc.setParameters(VM_SCHEDULE_ID, scheduleId); + sc.setParameters(SCHEDULED_TIMESTAMP, scheduledTimestamp); + return findOneBy(sc); + } } diff --git a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java index 7c5692564c99..a26ac028529a 100644 --- a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java +++ b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java @@ -154,7 +154,7 @@ private byte[] validateAndDecodeByHTTPMethod(String userData, int maxHTTPLength, throw new InvalidParameterValueException("User data is too short."); } - logger.trace(String.format("Decoded user data: [%s].", decodedUserData)); + logger.trace(String.format("Decoded user data: [%s].", new String(decodedUserData))); int userDataLength = userData.length(); int decodedUserDataLength = decodedUserData.length; logger.info(String.format("Configured base64 encoded user data size: %d bytes, actual user data size: %d bytes", userDataLength, decodedUserDataLength)); diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java index 7672b9dc6f97..87b3f84f30d0 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java @@ -1153,7 +1153,7 @@ public void doInTransactionWithoutResult(TransactionStatus status) { final List jobs = _jobDao.getResetJobs(msid); for (final AsyncJobVO job : jobs) { if (logger.isDebugEnabled()) { - logger.debug("Cancel left-over job-" + job.getId()); + logger.debug("Cancel left-over job-{} for msid {}", job.getId(), msid); } cleanupResources(job); job.setStatus(JobInfo.Status.FAILED); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 0aa094e56d99..14b9006597d7 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -533,6 +533,9 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv POWER_STATES_TABLE.put(DomainState.VIR_DOMAIN_BLOCKED, PowerState.PowerOn); POWER_STATES_TABLE.put(DomainState.VIR_DOMAIN_NOSTATE, PowerState.PowerUnknown); POWER_STATES_TABLE.put(DomainState.VIR_DOMAIN_SHUTDOWN, PowerState.PowerOff); + // Report crashed domains as PowerOff immediately instead of omitting them + // from the report (which delays detection via the "missing VM" threshold). + POWER_STATES_TABLE.put(DomainState.VIR_DOMAIN_CRASHED, PowerState.PowerOff); } public VirtualRoutingResource virtRouterResource; @@ -5788,7 +5791,7 @@ public List> cleanVMSnapshotMetadata(Domain dm) } for (String snapshotName: snapshotNames) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("Cleaning snapshot [%s] of VM [%s] metadata.", snapshotNames, dm.getName())); + LOGGER.debug(String.format("Cleaning snapshot [%s] of VM [%s] metadata.", snapshotName, dm.getName())); } DomainSnapshot snapshot = dm.snapshotLookupByName(snapshotName); snapshot.delete(flags); // clean metadata of vm snapshot diff --git a/plugins/storage/volume/primera/src/main/java/org/apache/cloudstack/storage/datastore/adapter/primera/PrimeraAdapter.java b/plugins/storage/volume/primera/src/main/java/org/apache/cloudstack/storage/datastore/adapter/primera/PrimeraAdapter.java index b574ccdb1649..3f92f8e4014d 100644 --- a/plugins/storage/volume/primera/src/main/java/org/apache/cloudstack/storage/datastore/adapter/primera/PrimeraAdapter.java +++ b/plugins/storage/volume/primera/src/main/java/org/apache/cloudstack/storage/datastore/adapter/primera/PrimeraAdapter.java @@ -384,6 +384,7 @@ public ProviderVolume copy(ProviderAdapterContext context, ProviderAdapterDataOb // Online copy configuration: immediate clone with deduplication and compression parms.setOnline(true); parms.setDestCPG(cpg); + parms.setSnapCPG(snapCpg); parms.setTpvv(false); parms.setReduce(true); logger.debug("PrimeraAdapter: Configuring online copy - destination CPG: '{}', deduplication enabled, thin provisioning disabled", cpg); diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index c78ac05102f8..4cd14ee7bf33 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -718,7 +718,7 @@ public String handleRequest(final Map params, final String responseType, final S logger.info(ex.getDescription()); throw ex; } catch (final Exception ex) { - logger.error("unhandled exception executing api command: " + ((command == null) ? "null" : command), ex); + logger.error("unhandled exception executing api command: " + ((command == null || command.length == 0) ? "null" : command[0]), ex); String errorMsg = ex.getMessage(); if (!accountMgr.isRootAdmin(CallContext.current().getCallingAccount().getId())) { // hide internal details to non-admin user for security reason diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index 21d093758127..98dc28afb156 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -302,7 +302,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp try { if (LOGGER.isTraceEnabled()) { LOGGER.trace(String.format("apiAuthenticator.authenticate(%s, params[%d], %s, %s, %s, %s, %s,%s)", - saveLogString(command), params.size(), session.getId(), remoteAddress.getHostAddress(), saveLogString(responseType), "auditTrailSb", "req", "resp")); + saveLogString(command), params.size(), session != null ? session.getId() : null, remoteAddress.getHostAddress(), saveLogString(responseType), "auditTrailSb", "req", "resp")); } responseString = apiAuthenticator.authenticate(command, params, session, remoteAddress, responseType, auditTrailSb, req, resp); if (session != null && session.getAttribute(ApiConstants.SESSIONKEY) != null) { @@ -316,22 +316,20 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp } if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGOUT_API) { - if (session == null) { - throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Session not found for the logout process."); - } + if (session != null) { - final Long userId = (Long) session.getAttribute("userid"); - final Account account = (Account) session.getAttribute("accountobj"); - Long accountId = null; - if (account != null) { - accountId = account.getId(); - } - auditTrailSb.insert(0, "(userId=" + userId + " accountId=" + accountId + " sessionId=" + session.getId() + ")"); - if (userId != null) { - apiServer.logoutUser(userId); + final Long userId = (Long) session.getAttribute("userid"); + final Account account = (Account) session.getAttribute("accountobj"); + Long accountId = null; + if (account != null) { + accountId = account.getId(); + } + auditTrailSb.insert(0, "(userId=" + userId + " accountId=" + accountId + " sessionId=" + session.getId() + ")"); + if (userId != null) { + apiServer.logoutUser(userId); + } + invalidateHttpSession(session, "invalidating session after logout call"); } - invalidateHttpSession(session, "invalidating session after logout call"); - final Cookie[] cookies = req.getCookies(); if (cookies != null) { for (final Cookie cookie : cookies) { diff --git a/server/src/main/java/com/cloud/hypervisor/CloudZonesStartupProcessor.java b/server/src/main/java/com/cloud/hypervisor/CloudZonesStartupProcessor.java index f2704f35fcdd..35b3a0498cfa 100644 --- a/server/src/main/java/com/cloud/hypervisor/CloudZonesStartupProcessor.java +++ b/server/src/main/java/com/cloud/hypervisor/CloudZonesStartupProcessor.java @@ -24,6 +24,8 @@ import org.springframework.stereotype.Component; +import org.apache.cloudstack.utils.identity.ManagementServerNode; + import com.cloud.agent.AgentManager; import com.cloud.agent.StartupCommandProcessor; import com.cloud.agent.api.StartupCommand; @@ -38,7 +40,6 @@ import com.cloud.host.HostVO; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.utils.component.AdapterBase; -import com.cloud.utils.net.MacAddress; import com.cloud.utils.net.NetUtils; /** @@ -62,7 +63,7 @@ public boolean configure(String name, Map params) throws Configu if (_nodeId == -1) { // FIXME: We really should not do this like this. It should be done // at config time and is stored as a config variable. - _nodeId = MacAddress.getMacAddress().toLong(); + _nodeId = ManagementServerNode.getManagementServerId(); } return true; } diff --git a/server/src/main/java/com/cloud/network/NetworkUsageManagerImpl.java b/server/src/main/java/com/cloud/network/NetworkUsageManagerImpl.java index 0aee5f234969..3d512be0d31e 100644 --- a/server/src/main/java/com/cloud/network/NetworkUsageManagerImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkUsageManagerImpl.java @@ -34,6 +34,7 @@ import org.apache.cloudstack.api.command.admin.usage.DeleteTrafficMonitorCmd; import org.apache.cloudstack.api.command.admin.usage.ListTrafficMonitorsCmd; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.utils.identity.ManagementServerNode; import com.cloud.agent.AgentManager; import com.cloud.agent.Listener; @@ -88,7 +89,6 @@ import com.cloud.utils.db.TransactionCallbackNoReturn; import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.utils.net.MacAddress; @Component public class NetworkUsageManagerImpl extends ManagerBase implements NetworkUsageService, NetworkUsageManager, ResourceStateAdapter { @@ -251,7 +251,7 @@ protected class DirectNetworkStatsListener implements Listener { private int _interval; - private final long mgmtSrvrId = MacAddress.getMacAddress().toLong(); + private final long mgmtSrvrId = ManagementServerNode.getManagementServerId(); protected DirectNetworkStatsListener(int interval) { _interval = interval; diff --git a/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java b/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java index d026bd914a31..15e62bb7e9de 100644 --- a/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java +++ b/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java @@ -44,6 +44,7 @@ import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; +import com.cloud.agent.api.routing.GroupAnswer; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.manager.Commands; import com.cloud.alert.AlertManager; @@ -211,6 +212,8 @@ public boolean sendCommandsToRouter(final VirtualRouter router, final Commands c } if (answers == null || answers.length != cmds.size()) { + logger.error(String.format("VIRTUAL_ROUTER_COMMUNICATION_ERROR: Received %s answers for %s commands from router [%s] (ID: %s, HostId: %s)", + (answers == null ? 0 : answers.length), cmds.size(), router.getInstanceName(), router.getId(), router.getHostId())); return false; } @@ -219,6 +222,15 @@ public boolean sendCommandsToRouter(final VirtualRouter router, final Commands c for (final Answer answer : answers) { if (!answer.getResult()) { result = false; + String errorDetails = answer.getDetails(); + if (answer instanceof GroupAnswer) { + GroupAnswer groupAnswer = (GroupAnswer) answer; + if (groupAnswer.getResults() != null) { + errorDetails = String.join(" | ", groupAnswer.getResults()); + } + } + logger.error(String.format("VIRTUAL_ROUTER_COMMAND_FAILED: Router [%s] (ID: %s, HostId: %s) failed executing command [%s]. Error details: %s", + router.getInstanceName(), router.getId(), router.getHostId(), answer.getClass().getSimpleName(), errorDetails)); break; } } diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index e881bd7ac6e8..4bce7c4ca0c6 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -250,7 +250,6 @@ import com.cloud.utils.fsm.StateListener; import com.cloud.utils.fsm.StateMachine2; import com.cloud.utils.net.Ip; -import com.cloud.utils.net.MacAddress; import com.cloud.utils.net.NetUtils; import com.cloud.vm.DomainRouterVO; import com.cloud.vm.Nic; @@ -365,7 +364,7 @@ public class VirtualNetworkApplianceManagerImpl extends ManagerBase implements V private int _routerExtraPublicNics = 2; private int _usageAggregationRange = 1440; private String _usageTimeZone = "GMT"; - private final long mgmtSrvrId = MacAddress.getMacAddress().toLong(); + private final long mgmtSrvrId = ManagementServerNode.getManagementServerId(); private static final int ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION = 5; // 5 seconds private boolean _dailyOrHourly = false; diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 3f811c152f00..91b9bd13fa30 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -843,7 +843,6 @@ import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.fsm.StateMachine2; -import com.cloud.utils.net.MacAddress; import com.cloud.utils.net.NetUtils; import com.cloud.utils.security.CertificateHelper; import com.cloud.utils.ssh.SSHKeysHelper; @@ -1196,7 +1195,7 @@ public DetailVO findDetail(final long hostId, final String name) { @Override public long getId() { - return MacAddress.getMacAddress().toLong(); + return ManagementServerNode.getManagementServerId(); } protected void checkPortParameters(final String publicPort, final String privatePort, final String privateIp, final String proto) { diff --git a/server/src/main/java/com/cloud/server/StatsCollector.java b/server/src/main/java/com/cloud/server/StatsCollector.java index a0d9c09c3061..566bc30d6747 100644 --- a/server/src/main/java/com/cloud/server/StatsCollector.java +++ b/server/src/main/java/com/cloud/server/StatsCollector.java @@ -147,7 +147,6 @@ import com.cloud.utils.db.TransactionCallbackNoReturn; import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.utils.net.MacAddress; import com.cloud.utils.script.Script; import com.cloud.vm.NicVO; import com.cloud.vm.UserVmManager; @@ -382,7 +381,7 @@ public String toString() { private ScheduledExecutorService _diskStatsUpdateExecutor; private int _usageAggregationRange = 1440; private String _usageTimeZone = "GMT"; - private final long mgmtSrvrId = MacAddress.getMacAddress().toLong(); + private final long mgmtSrvrId = ManagementServerNode.getManagementServerId(); private static final int ACQUIRE_GLOBAL_LOCK_TIMEOUT_FOR_COOPERATION = 5; // 5 seconds private boolean _dailyOrHourly = false; protected long managementServerNodeId = ManagementServerNode.getManagementServerId(); diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index df0283ae2d61..06c3ab3bcfbc 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -2887,7 +2887,7 @@ public boolean checkIfHostAndStoragePoolHasCommonStorageAccessGroups(Host host, } if (ArrayUtils.isNotEmpty(hostStorageAccessGroups)) { - logger.debug(String.format("Storage access groups on the host %s are %s", host, hostStorageAccessGroups)); + logger.debug(String.format("Storage access groups on the host %s are %s", host, Arrays.toString(hostStorageAccessGroups))); } if (CollectionUtils.isNotEmpty(storagePoolAccessGroups)) { diff --git a/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkTopology.java b/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkTopology.java index a7000f702ec9..abb04f932c06 100644 --- a/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkTopology.java +++ b/server/src/main/java/org/apache/cloudstack/network/topology/BasicNetworkTopology.java @@ -398,6 +398,8 @@ public boolean applyRules(final Network network, final VirtualRouter router, fin // If rules fail to apply on one domR and not due to // disconnection, no need to proceed with the rest if (!result) { + logger.error(String.format("VIRTUAL_ROUTER_RULE_FAILURE: Failed to apply [%s] on Virtual Router [%s] (ID: %s, HostId: %s, NetworkId: %s). Router requires restart/bounce.", + typeString, router.getInstanceName(), router.getId(), router.getHostId(), network.getId())); if (isZoneBasic && isPodLevelException) { throw new ResourceUnavailableException("Unable to apply " + typeString + " on router ", Pod.class, podId); } diff --git a/server/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedulerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedulerImpl.java index 7410fb1c2655..56d794fa5c2c 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedulerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedulerImpl.java @@ -162,7 +162,13 @@ public Date scheduleNextJob(VMScheduleVO vmSchedule, Date timestamp) { } Date scheduledDateTime = Date.from(ts.toInstant()); - VMScheduledJobVO scheduledJob = new VMScheduledJobVO(vmSchedule.getVmId(), vmSchedule.getId(), vmSchedule.getAction(), scheduledDateTime); + VMScheduledJobVO scheduledJob = vmScheduledJobDao.findByScheduleAndTimestamp(vmSchedule.getId(), scheduledDateTime); + if (scheduledJob != null) { + logger.trace("Job is already scheduled for schedule {} at {}", vmSchedule, scheduledDateTime); + return scheduledDateTime; + } + + scheduledJob = new VMScheduledJobVO(vmSchedule.getVmId(), vmSchedule.getId(), vmSchedule.getAction(), scheduledDateTime); try { vmScheduledJobDao.persist(scheduledJob); ActionEventUtils.onScheduledActionEvent(User.UID_SYSTEM, vm.getAccountId(), actionEventMap.get(vmSchedule.getAction()), diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 4f450e940fc0..8764959c5ead 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -44,6 +44,7 @@ "label.accounttype": "Account type", "label.acl.export": "Export ACL rules", "label.acl.id": "ACL ID", +"label.aclname": "ACL name", "label.acl.rules": "ACL rules", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", diff --git a/ui/src/permission.js b/ui/src/permission.js index 266dc992c8db..333c73f8ffbd 100644 --- a/ui/src/permission.js +++ b/ui/src/permission.js @@ -107,11 +107,7 @@ router.beforeEach((to, from, next) => { router.addRoute(route) }) const redirect = decodeURIComponent(from.query.redirect || to.path) - if (to.path === redirect) { - next({ ...to, replace: true }) - } else { - next({ path: redirect }) - } + next({ path: redirect, replace: true, query: to.query, hash: to.hash }) var project = vueProps.$localStorage.get(CURRENT_PROJECT) if (project == null) { project = {} diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index 2c0edf656d73..526711d9ad49 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -63,7 +63,7 @@ const user = { headerNotices: [], isLdapEnabled: false, cloudian: {}, - zones: {}, + zones: [], timezoneoffset: 0.0, usebrowsertimezone: false, domainStore: {}, @@ -344,13 +344,12 @@ const user = { }) } else if (store.getters.loginFlag) { const hide = message.loading(i18n.global.t('message.discovering.feature'), 0) - getAPI('listZones').then(json => { + Promise.all([ + getAPI('listZones'), + getAPI('listApis') + ]).then(([json, response]) => { const zones = json.listzonesresponse.zone || [] commit('SET_ZONES', zones) - }).catch(error => { - reject(error) - }) - getAPI('listApis').then(response => { const apis = {} const apiList = response.listapisresponse.api for (var idx = 0; idx < apiList.length; idx++) { diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue index 8c46ca64225c..e0583cd97a4e 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -1997,8 +1997,13 @@ export default { }, onSearch (opts) { const query = Object.assign({}, this.$route.query) - const searchFilters = this.$route?.meta?.searchFilters || [] - searchFilters.forEach(key => delete query[key]) + let searchFilters = this.$route?.meta?.searchFilters || [] + if (typeof searchFilters === 'function') { + searchFilters = searchFilters() + } + if (Array.isArray(searchFilters)) { + searchFilters.forEach(key => delete query[key]) + } delete query.name delete query.templatetype delete query.keyword diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index 423338c92641..2b0bc92b3823 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -148,6 +148,7 @@ @handle-image-search-filter="filters => fetchImages(filters)" @update-image="updateFieldValue" @update-disk-size="updateFieldValue" + @change-root-disk-override-checked="val => { showRootDiskSizeChanger = val; form.rootdisksizeitem = val }" @change-iso-hypervisor="value => form.hypervisor = value" /> { - this.networkOfferings = json.listnetworkofferingsresponse.networkoffering + this.networkOfferings = json.listnetworkofferingsresponse.networkoffering || [] this.networkOfferings = this.networkOfferings.filter(offering => offering.fornsx === this.selectedZone.isnsxenabled) if (!this.selectedZone.routedmodeenabled) { this.networkOfferings = this.networkOfferings.filter(offering => offering.networkmode !== 'ROUTED') diff --git a/ui/src/views/network/CreateNetworkPermission.vue b/ui/src/views/network/CreateNetworkPermission.vue index 4cd335bbfbe4..cb7062854c3d 100644 --- a/ui/src/views/network/CreateNetworkPermission.vue +++ b/ui/src/views/network/CreateNetworkPermission.vue @@ -49,6 +49,7 @@ api="listProjects" :apiParams="projectsApiParams" resourceType="project" + :pageSize="100" defaultIcon="project-outlined" /> @@ -121,7 +122,9 @@ export default { }, projectsApiParams () { return { - details: 'min' + details: 'min', + listall: true, + timestamp: this.timestamp } } }, diff --git a/ui/src/views/project/AccountsTab.vue b/ui/src/views/project/AccountsTab.vue index a3256b3bac2f..f49a7ed88a7a 100644 --- a/ui/src/views/project/AccountsTab.vue +++ b/ui/src/views/project/AccountsTab.vue @@ -166,6 +166,7 @@ export default { this.updateProjectApi = this.$store.getters.apis.updateProject if (!this.resource.id) return this.fetchUsers() + this.checkCurrentUserProjectAdmin() this.fetchProjectAccounts(params) if (this.isProjectRolesSupported()) { this.fetchProjectRoles() @@ -193,6 +194,25 @@ export default { } return false }, + checkCurrentUserProjectAdmin () { + if (['Admin', 'DomainAdmin'].includes(this.$store.getters.userInfo.roletype)) { + this.imProjectAdmin = true + return + } + getAPI('listProjectAccounts', { + projectId: this.resource.id, + account: this.$store.getters.userInfo.account, + userid: this.$store.getters.userInfo.id + }).then(json => { + const accounts = json.listprojectaccountsresponse.projectaccount || [] + for (const account of accounts) { + if (this.isLoggedInUserProjectAdmin(account)) { + this.imProjectAdmin = true + return + } + } + }).catch(() => {}) + }, isProjectRolesSupported () { return ('listProjectRoles' in this.$store.getters.apis) }, @@ -229,13 +249,6 @@ export default { this.dataSource = [] return } - for (const projectAccount of listProjectAccount) { - this.imProjectAdmin = this.isLoggedInUserProjectAdmin(projectAccount) - if (this.imProjectAdmin) { - break - } - } - this.itemCount = itemCount this.dataSource = listProjectAccount }).catch(error => { diff --git a/utils/src/main/java/org/apache/cloudstack/utils/identity/ManagementServerNode.java b/utils/src/main/java/org/apache/cloudstack/utils/identity/ManagementServerNode.java index 32989c3497c5..592c6e7e12aa 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/identity/ManagementServerNode.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/identity/ManagementServerNode.java @@ -19,7 +19,11 @@ package org.apache.cloudstack.utils.identity; - +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import com.cloud.utils.component.AdapterBase; import com.cloud.utils.component.ComponentLifecycle; @@ -27,9 +31,90 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.MacAddress; +/** + * Canonical source of the management-server node id ({@code msid}). + * + *

By default the id is derived from the host hardware MAC address. When the MAC address is + * not stable across restarts, the {@code msid} changes, which orphans the {@code mshost} row + * and breaks async jobs, HA work ({@code fk_op_ha_work__mgmt_server_id}), and router/stats + * ownership. + * + *

Setting the environment variable {@code CLOUDSTACK_MSID_FROM_FQDN=true} (or the system + * property {@code cloudstack.msid.from.fqdn=true}) instead derives the id from a SHA-256 hash + * of the node FQDN, which stays stable across restarts. All node-identity consumers must + * obtain the id from {@link #getManagementServerId()} so they agree on the same value. + */ public class ManagementServerNode extends AdapterBase implements SystemIntegrityChecker { - private static final long s_nodeId = MacAddress.getMacAddress().toLong(); + private static final String CS_MSID_FROM_ID_ENV_VAR = "CLOUDSTACK_MSID_FROM_ID"; + private static final String CS_MSID_FROM_ID_SYS_PROP = "cloudstack.msid.from.id"; + + // op_lock.mac is varchar(17) and holds the msid, so the id must stay within the 48-bit MAC address range. + private static final int MSID_BYTES = 6; + + private static final Logger s_logger = LogManager.getLogger(ManagementServerNode.class); + + private static String s_nodeIdSource; + private static long s_nodeId = initNodeId(); + + private static long initNodeId() { + + s_logger.info("Initializing management server node ID"); + // Check if CS_MSID_FROM_ID_ENV_VAR or CS_MSID_FROM_ID_SYS_PROP has a value + String msidFromEnvVal = System.getenv(CS_MSID_FROM_ID_ENV_VAR); + String msidFromPropVal = System.getProperty(CS_MSID_FROM_ID_SYS_PROP); + + String identity = null; + String identifyValue = null; + if (msidFromEnvVal != null) { + identifyValue = msidFromEnvVal; + identity = trimToNull(msidFromEnvVal); + s_nodeIdSource = "CS_MSID_FROM_ID_ENV_VAR"; + } + + if (msidFromPropVal != null) { + identifyValue = msidFromPropVal; + identity = trimToNull(msidFromPropVal); + s_nodeIdSource = "CS_MSID_FROM_ID_SYS_PROP"; + } + + if (identity != null) { + long identityHash = hashNodeIdentity(identity); + s_logger.info("Using {} for management server node ID: {} (identity: {}, hash: {})", s_nodeIdSource, identifyValue, identity, identityHash); + return identityHash; + } + + // Use mac address + s_nodeIdSource = "mac-address"; + long macAddress = MacAddress.getMacAddress().toLong(); + s_logger.info("Using {} for management server node ID: {}", s_nodeIdSource, macAddress); + + return macAddress; + } + + static long hashNodeIdentity(String nodeIdentity) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(nodeIdentity.getBytes(StandardCharsets.UTF_8)); + long id = 0; + for (int i = 0; i < MSID_BYTES; i++) { + id = (id << 8) | (hash[i] & 0xFFL); + } + + return id == 0 ? 1 : id; + } catch (NoSuchAlgorithmException e) { + throw new CloudRuntimeException("SHA-256 algorithm not available for management server ID generation", e); + } + } + + private static String trimToNull(String value) { + if (value == null) { + return null; + } + + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } public ManagementServerNode() { setRunLevel(ComponentLifecycle.RUN_LEVEL_FRAMEWORK_BOOTSTRAP); @@ -54,6 +139,8 @@ public boolean start() { logger.error("System integrity check exception", e); System.exit(1); } + + logger.info("Management server node id: {} (source: {})", s_nodeId, s_nodeIdSource); return true; } } diff --git a/utils/src/test/java/org/apache/cloudstack/utils/identity/ManagementServerNodeTest.java b/utils/src/test/java/org/apache/cloudstack/utils/identity/ManagementServerNodeTest.java new file mode 100644 index 000000000000..78d57c91812d --- /dev/null +++ b/utils/src/test/java/org/apache/cloudstack/utils/identity/ManagementServerNodeTest.java @@ -0,0 +1,88 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.utils.identity; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +import org.junit.Test; + +public class ManagementServerNodeTest { + + private static String invokeTrimToNull(String value) throws Exception { + Method m = ManagementServerNode.class.getDeclaredMethod("trimToNull", String.class); + m.setAccessible(true); + return (String) m.invoke(null, value); + } + + @Test + public void testGetManagementServerIdIsPositive() { + assertTrue("Node id must be a positive, non-zero value", ManagementServerNode.getManagementServerId() > 0); + } + + @Test + public void testCheckPassesWithValidNodeId() { + // Node id is derived at class-load time and should be valid, so check() must not throw. + new ManagementServerNode().check(); + } + + @Test + public void testStartReturnsTrueWithValidNodeId() { + // With a valid node id, start() must succeed and return true without terminating the JVM. + assertTrue(new ManagementServerNode().start()); + } + + @Test + public void testHashNodeIdentityIsDeterministicAndFitsInMacAddressRange() { + long first = ManagementServerNode.hashNodeIdentity("cloudstack-mgmt-0.cloudstack-mgmt.cloudstack-mgmt.svc.cluster.local"); + long second = ManagementServerNode.hashNodeIdentity("cloudstack-mgmt-0.cloudstack-mgmt.cloudstack-mgmt.svc.cluster.local"); + + System.out.println("First hash: " + first); + System.out.println("Second hash: " + second); + + assertEquals(first, second); + assertTrue(first > 0); + assertTrue(first <= 0xFFFFFFFFFFFFL); + } + + @Test + public void testHashNodeIdentityUsesFirstSixSha256Bytes() throws Exception { + String identity = "cloudstack-mgmt-0.cloudstack-mgmt.cloudstack-mgmt.svc.cluster.local"; + byte[] hash = MessageDigest.getInstance("SHA-256").digest(identity.getBytes(StandardCharsets.UTF_8)); + long expected = 0; + for (int i = 0; i < 6; i++) { + expected = (expected << 8) | (hash[i] & 0xFFL); + } + + assertEquals(expected, ManagementServerNode.hashNodeIdentity(identity)); + } + + @Test + public void testTrimToNullReturnsTrimmedValue() throws Exception { + assertEquals("management.example.test", invokeTrimToNull(" management.example.test \n")); + } + + @Test + public void testTrimToNullReturnsNullForNullAndBlankValues() throws Exception { + assertEquals(null, invokeTrimToNull(null)); + assertEquals(null, invokeTrimToNull(" \t\n ")); + } +}