From 05c020e1f6679edb74ef9ee513fc6240db981139 Mon Sep 17 00:00:00 2001 From: "Manuel Amador (Rudd-O)" Date: Wed, 11 Aug 2010 09:13:29 -0700 Subject: [PATCH] Source code committed --- HACKING | 652 + INSTALL | 155 + README | 52 + README.html | 10555 ++++++++++++++++ agent/.classpath | 16 + agent/.project | 17 + agent/bindir/cloud-setup-agent.in | 109 + agent/conf/agent.properties | 30 + agent/conf/developer.properties.template | 38 + agent/conf/environment.properties.in | 3 + agent/conf/log4j-cloud.xml.in | 75 + .../SYSCONFDIR/rc.d/init.d/cloud-agent.in | 81 + .../SYSCONFDIR/rc.d/init.d/cloud-agent.in | 81 + .../ubuntu/SYSCONFDIR/init.d/cloud-agent.in | 103 + .../ubuntu/SYSCONFDIR/init/cloud-cleanup.conf | 15 + agent/doc/README-iscsi.txt | 230 + agent/doc/README.txt | 194 + agent/libexec/agent-runner.in | 71 + agent/patch/patch.tgz | Bin 0 -> 2540 bytes agent/patch/redopatch.sh | 6 + agent/scripts/agent.sh | 13 + agent/scripts/run.sh | 3 + agent/src/com/cloud/agent/Agent.java | 801 ++ agent/src/com/cloud/agent/AgentShell.java | 590 + agent/src/com/cloud/agent/IAgentShell.java | 46 + .../com/cloud/agent/dao/StorageComponent.java | 30 + .../agent/dao/impl/PropertiesStorage.java | 131 + .../cloud/agent/resource/DummyResource.java | 104 + .../computing/LibvirtCapXMLParser.java | 224 + .../computing/LibvirtComputingResource.java | 3551 ++++++ .../computing/LibvirtDomainXMLParser.java | 178 + .../resource/computing/LibvirtNetworkDef.java | 173 + .../computing/LibvirtStoragePoolDef.java | 70 + .../computing/LibvirtStorageVolumeDef.java | 70 + .../resource/computing/LibvirtVMDef.java | 653 + .../resource/computing/LibvirtXMLParser.java | 85 + .../consoleproxy/ConsoleProxyResource.java | 353 + .../resource/storage/IscsiMountPreparer.java | 224 + .../test/com/cloud/agent/TestAgentShell.java | 46 + api/.classpath | 7 + api/.project | 17 + .../cloud/deploy/DataCenterDeployment.java | 29 + .../com/cloud/deploy/DeploymentStrategy.java | 26 + .../exception/AgentUnavailableException.java | 45 + .../ConcurrentOperationException.java | 29 + .../cloud/exception/DiscoveryException.java | 33 + .../com/cloud/exception/HAStateException.java | 35 + .../InsufficientAddressCapacityException.java | 38 + .../InsufficientCapacityException.java | 35 + .../InsufficientStorageCapacityException.java | 33 + ...ficientVirtualNetworkCapcityException.java | 28 + .../exception/InternalErrorException.java | 32 + .../InvalidParameterValueException.java | 34 + .../exception/ManagementServerException.java | 44 + .../NetworkRuleConflictException.java | 30 + .../exception/PermissionDeniedException.java | 33 + .../ResourceAllocationException.java | 38 + .../exception/ResourceInUseException.java | 56 + .../StorageUnavailableException.java | 37 + api/src/com/cloud/hypervisor/Hypervisor.java | 29 + api/src/com/cloud/network/Network.java | 61 + api/src/com/cloud/offering/DiskOffering.java | 44 + .../com/cloud/offering/NetworkOffering.java | 63 + .../com/cloud/offering/OfferingManager.java | 46 + .../com/cloud/offering/ServiceOffering.java | 76 + api/src/com/cloud/storage/Storage.java | 87 + api/src/com/cloud/uservm/UserVm.java | 71 + api/src/com/cloud/vm/Nic.java | 50 + api/src/com/cloud/vm/State.java | 99 + api/src/com/cloud/vm/VirtualMachine.java | 124 + api/src/com/cloud/vm/VmCharacteristics.java | 68 + build.xml | 78 + build/build-cloud.properties | 40 + build/build-cloud.xml | 566 + build/build-common.xml | 82 + build/build-docs.xml | 66 + build/build.number | 3 + build/cloud.properties | 14 + .../branding/default/images/favicon.ico | Bin 0 -> 1406 bytes .../branding/default/images/header_logo.gif | Bin 0 -> 5319 bytes .../branding/godaddy/images/header_logo.gif | Bin 0 -> 3047 bytes .../branding/nframe/images/header_logo.gif | Bin 0 -> 2774 bytes .../branding/superb/images/header_logo.gif | Bin 0 -> 2837 bytes build/deploy/db/deploy-db.sh | 103 + build/deploy/db/log4j.properties | 7 + build/deploy/deploy-agent.sh | 217 + build/deploy/deploy-console-proxy.sh | 73 + build/deploy/deploy-server.sh | 106 + build/deploy/deploy-simulator.sh | 185 + build/deploy/install-storage-server.sh | 133 + build/deploy/install.sh | 139 + .../agent/storagehdpatch/etc/default/init | 38 + .../agent/storagehdpatch/etc/inet/ntp.conf | 6 + .../agent/storagehdpatch/etc/nsswitch.conf | 70 + .../agent/storagehdpatch/etc/ssh/sshd_config | 154 + .../agent/storagehdpatch/etc/system | 101 + .../storagehdpatch/etc/vmops/disks.properties | 7 + .../etc/vmops/network.properties | 35 + .../agent/storagehdpatch/lib/svc/method/vmops | 106 + .../agent/storagehdpatch/usr/sbin/vsetup | 44 + .../var/svc/manifest/application/cloud.xml | 43 + .../consoleproxy/conf/consoleproxy.properties | 6 + .../deploy/production/db/server-setup-dev.xml | 532 + build/deploy/production/db/templates-dev.sql | 14 + .../premium/conf/log4j-cloud_usage.xml | 68 + .../conf/log4j-cloud_usage.xml.template | 68 + .../premium/conf/usage-components.xml | 48 + .../server/conf/agent-update.properties | 1 + .../server/conf/cloud-localhost.pk12 | Bin 0 -> 1597 bytes .../deploy/production/server/conf/ehcache.xml | 527 + .../production/server/conf/log4j-cloud.xml | 90 + .../server/conf/log4j-cloud.xml.template | 90 + .../deploy/production/server/conf/server.xml | 147 + build/developer.xml | 187 + build/overview.html | 18 + build/package.xml | 245 + build/release-notes | 328 + build/replace.properties | 8 + client/.classpath | 11 + client/.project | 17 + client/WEB-INF/web.xml | 55 + client/bindir/cloud-setup-management.in | 288 + .../bindir/cloud-update-xenserver-licenses.in | 165 + .../rc.d/init.d/cloud-management.in | 41 + .../SYSCONFDIR/sysconfig/cloud-management.in | 6 + .../rc.d/init.d/cloud-management.in | 41 + .../SYSCONFDIR/sysconfig/cloud-management.in | 6 + .../SYSCONFDIR/init.d/cloud-management.in | 229 + client/tomcatconf/catalina.policy.in | 180 + client/tomcatconf/catalina.properties.in | 81 + client/tomcatconf/classpath.conf.in | 22 + client/tomcatconf/commands.properties.in | 210 + client/tomcatconf/components.xml.in | 236 + client/tomcatconf/context.xml.in | 35 + client/tomcatconf/db.properties.in | 40 + client/tomcatconf/ehcache.xml.in | 527 + client/tomcatconf/environment.properties.in | 4 + client/tomcatconf/log4j-cloud.xml.in | 101 + client/tomcatconf/logging.properties.in | 64 + client/tomcatconf/server.xml.in | 147 + client/tomcatconf/tomcat-users.xml.in | 31 + client/tomcatconf/tomcat6.conf.in | 56 + client/tomcatconf/web.xml.in | 1188 ++ cloud.spec | 765 ++ configure-info.in | 3 + console-proxy/.classpath | 9 + console-proxy/.project | 23 + .../bindir/cloud-setup-console-proxy.in | 187 + console-proxy/conf.dom0/agent.properties.in | 29 + .../conf.dom0/consoleproxy.properties.in | 6 + console-proxy/conf.dom0/log4j-cloud.xml.in | 83 + console-proxy/conf/agent.properties | 2 + console-proxy/conf/consoleproxy.properties | 6 + console-proxy/conf/log4j-cloud.xml | 84 + console-proxy/css/ajaxviewer.css | 86 + console-proxy/css/logger.css | 120 + .../rc.d/init.d/cloud-console-proxy.in | 81 + .../rc.d/init.d/cloud-console-proxy.in | 81 + .../SYSCONFDIR/init.d/cloud-console-proxy.in | 95 + console-proxy/images/back.gif | Bin 0 -> 149 bytes console-proxy/images/bright-green.png | Bin 0 -> 3903 bytes console-proxy/images/cad.gif | Bin 0 -> 918 bytes console-proxy/images/cannotconnect.jpg | Bin 0 -> 1810 bytes console-proxy/images/clr_button.gif | Bin 0 -> 1274 bytes console-proxy/images/clr_button_hover.gif | Bin 0 -> 437 bytes console-proxy/images/dot.cur | Bin 0 -> 326 bytes console-proxy/images/gray-green.png | Bin 0 -> 3833 bytes console-proxy/images/grid_headerbg.gif | Bin 0 -> 196 bytes console-proxy/images/left.png | Bin 0 -> 3024 bytes console-proxy/images/minimize_button.gif | Bin 0 -> 634 bytes .../images/minimize_button_hover.gif | Bin 0 -> 227 bytes console-proxy/images/notready.jpg | Bin 0 -> 1827 bytes console-proxy/images/play_button.gif | Bin 0 -> 657 bytes console-proxy/images/play_button_hover.gif | Bin 0 -> 243 bytes console-proxy/images/right.png | Bin 0 -> 3131 bytes console-proxy/images/shrink_button.gif | Bin 0 -> 655 bytes console-proxy/images/shrink_button_hover.gif | Bin 0 -> 243 bytes console-proxy/images/stop_button.gif | Bin 0 -> 649 bytes console-proxy/images/stop_button_hover.gif | Bin 0 -> 231 bytes console-proxy/images/winlog.png | Bin 0 -> 2629 bytes console-proxy/js/ajaxviewer.js | 917 ++ console-proxy/js/cloud.logger.js | 288 + console-proxy/js/handler.js | 71 + console-proxy/js/jquery.js | 19 + console-proxy/libexec/console-proxy-runner.in | 71 + console-proxy/scripts/config_auth.sh | 43 + console-proxy/scripts/config_ssl.sh | 40 + console-proxy/scripts/run-proxy.sh | 27 + console-proxy/scripts/run.bat | 1 + console-proxy/scripts/run.sh | 51 + console-proxy/scripts/ssvm-check.sh | 117 + .../consoleproxy/AjaxFIFOImageCache.java | 82 + .../com/cloud/consoleproxy/ConsoleProxy.java | 682 + .../consoleproxy/ConsoleProxyAjaxHandler.java | 415 + .../ConsoleProxyAjaxImageHandler.java | 120 + .../ConsoleProxyAjaxKeyMapper.java | 248 + .../ConsoleProxyBaseServerFactoryImpl.java | 44 + .../ConsoleProxyClientHandler.java | 278 + .../consoleproxy/ConsoleProxyCmdHandler.java | 72 + .../consoleproxy/ConsoleProxyJarHandler.java | 108 + .../ConsoleProxyLoggerFactory.java | 88 + .../ConsoleProxyResourceHandler.java | 183 + .../ConsoleProxyServerFactory.java | 30 + .../consoleproxy/ConsoleProxyStatus.java | 61 + .../ConsoleProxyThumbnailHandler.java | 266 + .../consoleproxy/ConsoleProxyViewer.java | 1384 ++ .../com/cloud/consoleproxy/ViewerOptions.java | 38 + console-proxy/ui/viewer-bad-sid.ftl | 11 + console-proxy/ui/viewer-connect-failed.ftl | 11 + console-proxy/ui/viewer-update.ftl | 6 + console-proxy/ui/viewer.ftl | 41 + console-proxy/vm-script/vmops | 101 + console-viewer/.classpath | 7 + console-viewer/.project | 17 + console-viewer/scripts/run-viewer.bat | 5 + .../com/cloud/consoleviewer/AuthPanel.java | 118 + .../com/cloud/consoleviewer/ButtonPanel.java | 164 + .../cloud/consoleviewer/ClipboardFrame.java | 135 + .../cloud/consoleviewer/ConsoleApplet.java | 167 + .../cloud/consoleviewer/ConsoleViewer.java | 1247 ++ .../com/cloud/consoleviewer/OptionsFrame.java | 418 + .../cloud/consoleviewer/RecordingFrame.java | 314 + .../com/cloud/consoleviewer/ReloginPanel.java | 66 + console-viewer/test/test-applet.html | 48 + console/.classpath | 6 + console/.project | 17 + .../console/AuthenticationException.java | 36 + .../src/com/cloud/console/CapabilityInfo.java | 90 + .../src/com/cloud/console/CapsContainer.java | 109 + .../src/com/cloud/console/ConsoleCanvas.java | 2068 +++ .../src/com/cloud/console/ConsoleCanvas2.java | 63 + console/src/com/cloud/console/DesCipher.java | 496 + .../com/cloud/console/ITileScanListener.java | 27 + console/src/com/cloud/console/InStream.java | 180 + console/src/com/cloud/console/Logger.java | 225 + .../src/com/cloud/console/LoggerFactory.java | 23 + .../src/com/cloud/console/MemInStream.java | 35 + console/src/com/cloud/console/Region.java | 92 + .../com/cloud/console/RegionClassifier.java | 61 + console/src/com/cloud/console/RfbProto.java | 1474 +++ .../com/cloud/console/RfbProtoAdapter.java | 29 + console/src/com/cloud/console/RfbViewer.java | 57 + .../com/cloud/console/SessionRecorder.java | 196 + .../com/cloud/console/SplitInputStream.java | 86 + console/src/com/cloud/console/TileInfo.java | 57 + .../src/com/cloud/console/TileTracker.java | 271 + .../src/com/cloud/console/ZlibInStream.java | 114 + core/.classpath | 43 + core/.project | 17 + core/src/com/cloud/agent/AgentManager.java | 213 + core/src/com/cloud/agent/IAgentControl.java | 32 + .../cloud/agent/IAgentControlListener.java | 30 + core/src/com/cloud/agent/Listener.java | 120 + core/src/com/cloud/agent/RecoveryHandler.java | 32 + .../cloud/agent/api/AbstractStartCommand.java | 82 + .../cloud/agent/api/AgentControlAnswer.java | 32 + .../cloud/agent/api/AgentControlCommand.java | 29 + core/src/com/cloud/agent/api/Answer.java | 62 + .../com/cloud/agent/api/AttachIsoCommand.java | 52 + .../cloud/agent/api/AttachVolumeAnswer.java | 49 + .../cloud/agent/api/AttachVolumeCommand.java | 87 + .../cloud/agent/api/BackupSnapshotAnswer.java | 39 + .../agent/api/BackupSnapshotCommand.java | 95 + .../com/cloud/agent/api/CancelCommand.java | 46 + .../cloud/agent/api/ChangeAgentAnswer.java | 27 + .../cloud/agent/api/ChangeAgentCommand.java | 47 + .../cloud/agent/api/CheckHealthAnswer.java | 28 + .../cloud/agent/api/CheckHealthCommand.java | 29 + .../cloud/agent/api/CheckOnHostAnswer.java | 49 + .../cloud/agent/api/CheckOnHostCommand.java | 42 + .../com/cloud/agent/api/CheckOnVmAnswer.java | 52 + .../com/cloud/agent/api/CheckOnVmCommand.java | 41 + .../com/cloud/agent/api/CheckStateAnswer.java | 48 + .../cloud/agent/api/CheckStateCommand.java | 42 + .../agent/api/CheckVirtualMachineAnswer.java | 53 + .../agent/api/CheckVirtualMachineCommand.java | 41 + core/src/com/cloud/agent/api/Command.java | 40 + .../ConsoleAccessAuthenticationAnswer.java | 36 + .../ConsoleAccessAuthenticationCommand.java | 41 + .../api/ConsoleProxyLoadReportCommand.java | 41 + ...atePrivateTemplateFromSnapshotCommand.java | 80 + .../api/CreateVolumeFromSnapshotAnswer.java | 39 + .../api/CreateVolumeFromSnapshotCommand.java | 68 + .../cloud/agent/api/CreateZoneVlanAnswer.java | 33 + .../agent/api/CreateZoneVlanCommand.java | 45 + core/src/com/cloud/agent/api/CronCommand.java | 25 + .../agent/api/DeleteSnapshotBackupAnswer.java | 33 + .../api/DeleteSnapshotBackupCommand.java | 76 + .../agent/api/DeleteSnapshotsDirCommand.java | 66 + .../agent/api/DeleteStoragePoolCommand.java | 63 + core/src/com/cloud/agent/api/FenceAnswer.java | 37 + .../src/com/cloud/agent/api/FenceCommand.java | 56 + .../cloud/agent/api/GetFileStatsAnswer.java | 39 + .../cloud/agent/api/GetFileStatsCommand.java | 40 + .../cloud/agent/api/GetHostStatsAnswer.java | 118 + .../cloud/agent/api/GetHostStatsCommand.java | 53 + .../agent/api/GetStorageStatsAnswer.java | 49 + .../agent/api/GetStorageStatsCommand.java | 66 + .../com/cloud/agent/api/GetVmStatsAnswer.java | 39 + .../cloud/agent/api/GetVmStatsCommand.java | 54 + .../com/cloud/agent/api/GetVncPortAnswer.java | 38 + .../cloud/agent/api/GetVncPortCommand.java | 44 + .../com/cloud/agent/api/HostStatsEntry.java | 148 + .../com/cloud/agent/api/MaintainAnswer.java | 35 + .../com/cloud/agent/api/MaintainCommand.java | 29 + .../cloud/agent/api/ManageSnapshotAnswer.java | 42 + .../agent/api/ManageSnapshotCommand.java | 83 + .../com/cloud/agent/api/MigrateAnswer.java | 34 + .../com/cloud/agent/api/MigrateCommand.java | 51 + .../src/com/cloud/agent/api/MirrorAnswer.java | 85 + .../com/cloud/agent/api/MirrorCommand.java | 104 + .../cloud/agent/api/ModifySshKeysCommand.java | 43 + .../agent/api/ModifyStoragePoolAnswer.java | 62 + .../agent/api/ModifyStoragePoolCommand.java | 76 + .../agent/api/NetworkIngressRuleAnswer.java | 53 + .../agent/api/NetworkIngressRulesCmd.java | 159 + .../cloud/agent/api/NetworkUsageAnswer.java | 49 + .../cloud/agent/api/NetworkUsageCommand.java | 45 + core/src/com/cloud/agent/api/PingAnswer.java | 34 + core/src/com/cloud/agent/api/PingCommand.java | 46 + .../cloud/agent/api/PingRoutingCommand.java | 55 + .../api/PingRoutingWithNwGroupsCommand.java | 49 + .../cloud/agent/api/PingStorageCommand.java | 38 + .../com/cloud/agent/api/PingTestCommand.java | 54 + .../com/cloud/agent/api/PoolEjectCommand.java | 43 + .../agent/api/PrepareForMigrationAnswer.java | 27 + .../agent/api/PrepareForMigrationCommand.java | 44 + core/src/com/cloud/agent/api/ReadyAnswer.java | 32 + .../src/com/cloud/agent/api/ReadyCommand.java | 42 + .../src/com/cloud/agent/api/RebootAnswer.java | 66 + .../com/cloud/agent/api/RebootCommand.java | 49 + .../cloud/agent/api/RebootRouterCommand.java | 40 + .../api/SecStorageFirewallCfgCommand.java | 125 + .../agent/api/SecStorageSetupCommand.java | 68 + core/src/com/cloud/agent/api/SetupAnswer.java | 34 + .../src/com/cloud/agent/api/SetupCommand.java | 51 + .../com/cloud/agent/api/ShutdownCommand.java | 55 + .../com/cloud/agent/api/SnapshotCommand.java | 110 + core/src/com/cloud/agent/api/StartAnswer.java | 37 + .../src/com/cloud/agent/api/StartCommand.java | 198 + .../agent/api/StartConsoleProxyAnswer.java | 49 + .../agent/api/StartConsoleProxyCommand.java | 102 + .../cloud/agent/api/StartRouterAnswer.java | 54 + .../cloud/agent/api/StartRouterCommand.java | 82 + .../agent/api/StartSecStorageVmAnswer.java | 48 + .../agent/api/StartSecStorageVmCommand.java | 102 + .../com/cloud/agent/api/StartupAnswer.java | 49 + .../com/cloud/agent/api/StartupCommand.java | 261 + .../cloud/agent/api/StartupProxyCommand.java | 48 + .../agent/api/StartupRoutingCommand.java | 155 + .../agent/api/StartupStorageCommand.java | 106 + core/src/com/cloud/agent/api/StopAnswer.java | 50 + core/src/com/cloud/agent/api/StopCommand.java | 95 + .../com/cloud/agent/api/StoragePoolInfo.java | 98 + .../cloud/agent/api/UnsupportedAnswer.java | 28 + .../com/cloud/agent/api/UpgradeAnswer.java | 27 + .../com/cloud/agent/api/UpgradeCommand.java | 43 + .../agent/api/ValidateSnapshotAnswer.java | 54 + .../agent/api/ValidateSnapshotCommand.java | 85 + .../src/com/cloud/agent/api/VmStatsEntry.java | 87 + .../proxy/CheckConsoleProxyLoadCommand.java | 62 + .../api/proxy/ConsoleProxyLoadAnswer.java | 46 + .../cloud/agent/api/proxy/ProxyCommand.java | 26 + .../proxy/WatchConsoleProxyLoadCommand.java | 67 + .../agent/api/routing/DhcpEntryCommand.java | 61 + .../agent/api/routing/IPAssocCommand.java | 106 + .../api/routing/LoadBalancerCfgCommand.java | 76 + .../agent/api/routing/RoutingCommand.java | 26 + .../api/routing/SavePasswordCommand.java | 61 + .../api/routing/SetFirewallRuleCommand.java | 99 + .../agent/api/routing/UserDataCommand.java | 65 + .../agent/api/routing/VmDataCommand.java | 60 + .../api/storage/AbstractDownloadCommand.java | 68 + .../agent/api/storage/CopyVolumeAnswer.java | 44 + .../agent/api/storage/CopyVolumeCommand.java | 67 + .../cloud/agent/api/storage/CreateAnswer.java | 44 + .../agent/api/storage/CreateCommand.java | 92 + .../storage/CreatePrivateTemplateAnswer.java | 64 + .../storage/CreatePrivateTemplateCommand.java | 92 + .../api/storage/DeleteTemplateCommand.java | 44 + .../agent/api/storage/DestroyCommand.java | 47 + .../agent/api/storage/DownloadAnswer.java | 118 + .../agent/api/storage/DownloadCommand.java | 131 + .../api/storage/DownloadProgressCommand.java | 53 + .../ManageVolumeAvailabilityAnswer.java | 32 + .../ManageVolumeAvailabilityCommand.java | 56 + .../PrimaryStorageDownloadCommand.java | 61 + .../cloud/agent/api/storage/ShareAnswer.java | 44 + .../cloud/agent/api/storage/ShareCommand.java | 89 + .../agent/api/storage/StorageCommand.java | 27 + .../agent/api/storage/UpgradeDiskAnswer.java | 31 + .../agent/api/storage/UpgradeDiskCommand.java | 52 + .../agent/api/to/DiskCharacteristicsTO.java | 79 + core/src/com/cloud/agent/api/to/HostTO.java | 85 + .../src/com/cloud/agent/api/to/NetworkTO.java | 91 + .../com/cloud/agent/api/to/StoragePoolTO.java | 72 + .../com/cloud/agent/api/to/TemplateTO.java | 54 + core/src/com/cloud/agent/api/to/VmTO.java | 39 + core/src/com/cloud/agent/api/to/VolumeTO.java | 109 + .../VirtualRoutingResource.java | 616 + .../agent/transport/ArrayTypeAdaptor.java | 91 + .../com/cloud/agent/transport/Request.java | 391 + .../com/cloud/agent/transport/Response.java | 75 + .../agent/transport/UpgradeResponse.java | 98 + .../agent/transport/VolListTypeAdaptor.java | 60 + core/src/com/cloud/alert/AlertAdapter.java | 24 + core/src/com/cloud/alert/AlertManager.java | 48 + core/src/com/cloud/alert/AlertVO.java | 144 + core/src/com/cloud/alert/dao/AlertDao.java | 26 + .../src/com/cloud/alert/dao/AlertDaoImpl.java | 49 + .../async/AsyncInstanceCreateStatus.java | 26 + core/src/com/cloud/async/AsyncJobResult.java | 109 + core/src/com/cloud/async/AsyncJobVO.java | 331 + core/src/com/cloud/async/SyncQueueItemVO.java | 124 + core/src/com/cloud/async/SyncQueueVO.java | 141 + core/src/com/cloud/async/dao/AsyncJobDao.java | 30 + .../com/cloud/async/dao/AsyncJobDaoImpl.java | 81 + .../src/com/cloud/async/dao/SyncQueueDao.java | 27 + .../com/cloud/async/dao/SyncQueueDaoImpl.java | 79 + .../com/cloud/async/dao/SyncQueueItemDao.java | 30 + .../cloud/async/dao/SyncQueueItemDaoImpl.java | 110 + core/src/com/cloud/capacity/CapacityVO.java | 116 + .../com/cloud/capacity/dao/CapacityDao.java | 28 + .../cloud/capacity/dao/CapacityDaoImpl.java | 112 + .../src/com/cloud/cluster/ClusterManager.java | 60 + .../cloud/cluster/ClusterManagerListener.java | 26 + .../cluster/ClusterNodeJoinEventArgs.java | 45 + .../cluster/ClusterNodeLeftEventArgs.java | 46 + .../src/com/cloud/cluster/ClusterService.java | 28 + .../cloud/cluster/ManagementServerHostVO.java | 149 + .../cloud/cluster/RemoteMethodConstants.java | 26 + .../cluster/dao/ManagementServerHostDao.java | 34 + .../dao/ManagementServerHostDaoImpl.java | 129 + .../cloud/configuration/ConfigurationVO.java | 107 + .../cloud/configuration/ResourceCount.java | 51 + .../cloud/configuration/ResourceCountVO.java | 100 + .../cloud/configuration/ResourceLimit.java | 43 + .../cloud/configuration/ResourceLimitVO.java | 100 + .../configuration/dao/ConfigurationDao.java | 63 + .../dao/ConfigurationDaoImpl.java | 144 + .../configuration/dao/ResourceCountDao.java | 60 + .../dao/ResourceCountDaoImpl.java | 112 + .../configuration/dao/ResourceLimitDao.java | 35 + .../dao/ResourceLimitDaoImpl.java | 105 + .../ConsoleProxyAlertEventArgs.java | 73 + .../consoleproxy/ConsoleProxyAllocator.java | 29 + .../consoleproxy/ConsoleProxyListener.java | 92 + .../consoleproxy/ConsoleProxyManager.java | 54 + core/src/com/cloud/dc/AccountVlanMapVO.java | 63 + core/src/com/cloud/dc/ClusterVO.java | 89 + .../com/cloud/dc/DataCenterIpAddressVO.java | 96 + .../dc/DataCenterLinkLocalIpAddressVO.java | 96 + core/src/com/cloud/dc/DataCenterVO.java | 162 + core/src/com/cloud/dc/DataCenterVnetVO.java | 89 + core/src/com/cloud/dc/HostPodVO.java | 140 + core/src/com/cloud/dc/PodCluster.java | 68 + core/src/com/cloud/dc/PodVlanMapVO.java | 63 + core/src/com/cloud/dc/PodVlanVO.java | 97 + core/src/com/cloud/dc/Vlan.java | 47 + core/src/com/cloud/dc/VlanVO.java | 110 + .../com/cloud/dc/dao/AccountVlanMapDao.java | 32 + .../cloud/dc/dao/AccountVlanMapDaoImpl.java | 74 + core/src/com/cloud/dc/dao/ClusterDao.java | 28 + core/src/com/cloud/dc/dao/ClusterDaoImpl.java | 59 + core/src/com/cloud/dc/dao/DataCenterDao.java | 58 + .../com/cloud/dc/dao/DataCenterDaoImpl.java | 211 + .../dc/dao/DataCenterIpAddressDaoImpl.java | 208 + .../DataCenterLinkLocalIpAddressDaoImpl.java | 208 + .../cloud/dc/dao/DataCenterVnetDaoImpl.java | 151 + core/src/com/cloud/dc/dao/HostPodDao.java | 35 + core/src/com/cloud/dc/dao/HostPodDaoImpl.java | 289 + core/src/com/cloud/dc/dao/PodVlanDaoImpl.java | 138 + core/src/com/cloud/dc/dao/PodVlanMapDao.java | 32 + .../com/cloud/dc/dao/PodVlanMapDaoImpl.java | 74 + core/src/com/cloud/dc/dao/VlanDao.java | 51 + core/src/com/cloud/dc/dao/VlanDaoImpl.java | 258 + core/src/com/cloud/domain/DomainVO.java | 143 + core/src/com/cloud/domain/dao/DomainDao.java | 29 + .../com/cloud/domain/dao/DomainDaoImpl.java | 222 + core/src/com/cloud/event/EventState.java | 25 + core/src/com/cloud/event/EventTypes.java | 138 + core/src/com/cloud/event/EventVO.java | 176 + core/src/com/cloud/event/dao/EventDao.java | 37 + .../src/com/cloud/event/dao/EventDaoImpl.java | 85 + .../AgentControlChannelException.java | 27 + .../exception/OperationTimedoutException.java | 64 + .../UnsupportedVersionException.java | 42 + core/src/com/cloud/ha/FenceBuilder.java | 33 + .../com/cloud/ha/HighAvailabilityManager.java | 115 + core/src/com/cloud/ha/Investigator.java | 35 + core/src/com/cloud/ha/WorkVO.java | 204 + .../com/cloud/ha/dao/HighAvailabilityDao.java | 66 + .../cloud/ha/dao/HighAvailabilityDaoImpl.java | 174 + core/src/com/cloud/host/DetailVO.java | 72 + core/src/com/cloud/host/Host.java | 168 + core/src/com/cloud/host/HostEnvironment.java | 57 + core/src/com/cloud/host/HostInfo.java | 30 + core/src/com/cloud/host/HostStats.java | 38 + core/src/com/cloud/host/HostVO.java | 628 + .../InsufficientServerCapacityException.java | 42 + core/src/com/cloud/host/Status.java | 186 + core/src/com/cloud/host/dao/DetailsDao.java | 31 + .../com/cloud/host/dao/DetailsDaoImpl.java | 84 + core/src/com/cloud/host/dao/HostDao.java | 141 + core/src/com/cloud/host/dao/HostDaoImpl.java | 594 + .../vmware/resource/VmwareResource.java | 122 + .../xen/resource/CitrixResourceBase.java | 6164 +++++++++ .../xen/resource/XcpServerResource.java | 29 + .../xen/resource/XenServerConnectionPool.java | 637 + .../info/ConsoleProxyConnectionInfo.java | 31 + core/src/com/cloud/info/ConsoleProxyInfo.java | 91 + .../com/cloud/info/ConsoleProxyLoadInfo.java | 50 + .../com/cloud/info/ConsoleProxyStatus.java | 30 + .../com/cloud/info/RunningHostCountInfo.java | 50 + .../cloud/info/RunningHostInfoAgregator.java | 87 + .../com/cloud/info/SecStorageVmLoadInfo.java | 49 + core/src/com/cloud/maid/StackMaid.java | 123 + core/src/com/cloud/maid/StackMaidVO.java | 99 + core/src/com/cloud/maid/dao/StackMaidDao.java | 16 + .../com/cloud/maid/dao/StackMaidDaoImpl.java | 126 + .../network/BasicVirtualNetworkAllocator.java | 76 + .../network/ExteralIpAddressAllocator.java | 170 + .../src/com/cloud/network/FirewallRuleVO.java | 179 + .../cloud/network/HAProxyConfigurator.java | 183 + core/src/com/cloud/network/IPAddressVO.java | 120 + .../com/cloud/network/IpAddrAllocator.java | 40 + .../network/LoadBalancerConfigurator.java | 33 + .../cloud/network/LoadBalancerVMMapVO.java | 77 + .../src/com/cloud/network/LoadBalancerVO.java | 130 + core/src/com/cloud/network/NetworkEnums.java | 32 + .../cloud/network/NetworkRuleConfigVO.java | 94 + .../cloud/network/SecurityGroupVMMapVO.java | 68 + .../com/cloud/network/SecurityGroupVO.java | 87 + .../network/VirtualNetworkAllocator.java | 51 + .../cloud/network/dao/FirewallRulesDao.java | 48 + .../network/dao/FirewallRulesDaoImpl.java | 310 + .../com/cloud/network/dao/IPAddressDao.java | 54 + .../cloud/network/dao/IPAddressDaoImpl.java | 242 + .../cloud/network/dao/LoadBalancerDao.java | 31 + .../network/dao/LoadBalancerDaoImpl.java | 110 + .../network/dao/LoadBalancerVMMapDao.java | 32 + .../network/dao/LoadBalancerVMMapDaoImpl.java | 76 + .../network/dao/NetworkRuleConfigDao.java | 29 + .../network/dao/NetworkRuleConfigDaoImpl.java | 51 + .../cloud/network/dao/SecurityGroupDao.java | 30 + .../network/dao/SecurityGroupDaoImpl.java | 97 + .../network/dao/SecurityGroupVMMapDao.java | 31 + .../dao/SecurityGroupVMMapDaoImpl.java | 84 + .../cloud/network/security/IngressRuleVO.java | 133 + .../network/security/NetworkGroupRulesVO.java | 135 + .../network/security/NetworkGroupVMMapVO.java | 94 + .../network/security/NetworkGroupVO.java | 86 + .../network/security/NetworkGroupWorkVO.java | 139 + .../network/security/VmRulesetLogVO.java | 82 + .../network/security/dao/IngressRuleDao.java | 36 + .../security/dao/IngressRuleDaoImpl.java | 166 + .../network/security/dao/NetworkGroupDao.java | 32 + .../security/dao/NetworkGroupDaoImpl.java | 129 + .../security/dao/NetworkGroupRulesDao.java | 37 + .../dao/NetworkGroupRulesDaoImpl.java | 91 + .../security/dao/NetworkGroupVMMapDao.java | 36 + .../dao/NetworkGroupVMMapDaoImpl.java | 136 + .../security/dao/NetworkGroupWorkDao.java | 45 + .../security/dao/NetworkGroupWorkDaoImpl.java | 209 + .../network/security/dao/VmRulesetLogDao.java | 27 + .../security/dao/VmRulesetLogDaoImpl.java | 49 + .../cloud/offerings/NetworkOfferingVO.java | 105 + core/src/com/cloud/resource/DiskPreparer.java | 43 + .../com/cloud/resource/NetworkPreparer.java | 30 + .../com/cloud/resource/ServerResource.java | 74 + .../cloud/resource/ServerResourceBase.java | 304 + core/src/com/cloud/serializer/GsonHelper.java | 46 + core/src/com/cloud/serializer/Param.java | 28 + .../cloud/serializer/SerializerHelper.java | 172 + core/src/com/cloud/server/Criteria.java | 134 + .../com/cloud/server/ManagementServer.java | 2187 ++++ .../com/cloud/service/ServiceOfferingVO.java | 145 + .../cloud/service/dao/ServiceOfferingDao.java | 30 + .../service/dao/ServiceOfferingDaoImpl.java | 79 + .../src/com/cloud/storage/DiskOfferingVO.java | 239 + .../src/com/cloud/storage/DiskTemplateVO.java | 107 + .../storage/FileSystemStorageResource.java | 583 + core/src/com/cloud/storage/GuestOS.java | 26 + .../com/cloud/storage/GuestOSCategoryVO.java | 50 + core/src/com/cloud/storage/GuestOSVO.java | 71 + .../com/cloud/storage/JavaStorageLayer.java | 223 + .../com/cloud/storage/LaunchPermissionVO.java | 57 + .../com/cloud/storage/SecondaryStorage.java | 31 + .../cloud/storage/SecondaryStorageLayer.java | 40 + core/src/com/cloud/storage/Snapshot.java | 63 + .../cloud/storage/SnapshotPolicyRefVO.java | 57 + .../com/cloud/storage/SnapshotPolicyVO.java | 109 + .../com/cloud/storage/SnapshotScheduleVO.java | 102 + core/src/com/cloud/storage/SnapshotVO.java | 193 + core/src/com/cloud/storage/StorageLayer.java | 150 + .../src/com/cloud/storage/StorageManager.java | 291 + core/src/com/cloud/storage/StoragePool.java | 104 + .../cloud/storage/StoragePoolDetailVO.java | 80 + .../cloud/storage/StoragePoolDiscoverer.java | 34 + .../cloud/storage/StoragePoolHostAssoc.java | 38 + .../com/cloud/storage/StoragePoolHostVO.java | 105 + core/src/com/cloud/storage/StoragePoolVO.java | 271 + .../com/cloud/storage/StorageResource.java | 591 + core/src/com/cloud/storage/StorageStats.java | 29 + .../com/cloud/storage/VMTemplateHostVO.java | 266 + .../storage/VMTemplateStoragePoolVO.java | 217 + .../VMTemplateStorageResourceAssoc.java | 65 + core/src/com/cloud/storage/VMTemplateVO.java | 310 + .../com/cloud/storage/VMTemplateZoneVO.java | 117 + .../cloud/storage/VirtualMachineTemplate.java | 47 + core/src/com/cloud/storage/Volume.java | 80 + core/src/com/cloud/storage/VolumeStats.java | 26 + core/src/com/cloud/storage/VolumeVO.java | 493 + .../cloud/storage/dao/DiskOfferingDao.java | 30 + .../storage/dao/DiskOfferingDaoImpl.java | 95 + .../cloud/storage/dao/DiskTemplateDao.java | 26 + .../storage/dao/DiskTemplateDaoImpl.java | 53 + .../cloud/storage/dao/GuestOSCategoryDao.java | 25 + .../storage/dao/GuestOSCategoryDaoImpl.java | 32 + .../src/com/cloud/storage/dao/GuestOSDao.java | 25 + .../com/cloud/storage/dao/GuestOSDaoImpl.java | 32 + .../storage/dao/LaunchPermissionDao.java | 62 + .../storage/dao/LaunchPermissionDaoImpl.java | 158 + .../com/cloud/storage/dao/SnapshotDao.java | 33 + .../cloud/storage/dao/SnapshotDaoImpl.java | 86 + .../cloud/storage/dao/SnapshotPolicyDao.java | 35 + .../storage/dao/SnapshotPolicyDaoImpl.java | 81 + .../storage/dao/SnapshotPolicyRefDao.java | 35 + .../storage/dao/SnapshotPolicyRefDaoImpl.java | 83 + .../storage/dao/SnapshotScheduleDao.java | 39 + .../storage/dao/SnapshotScheduleDaoImpl.java | 106 + .../com/cloud/storage/dao/StoragePoolDao.java | 105 + .../cloud/storage/dao/StoragePoolDaoImpl.java | 377 + .../storage/dao/StoragePoolDetailsDao.java | 29 + .../dao/StoragePoolDetailsDaoImpl.java | 72 + .../cloud/storage/dao/StoragePoolHostDao.java | 46 + .../storage/dao/StoragePoolHostDaoImpl.java | 243 + .../com/cloud/storage/dao/VMTemplateDao.java | 62 + .../cloud/storage/dao/VMTemplateDaoImpl.java | 337 + .../cloud/storage/dao/VMTemplateHostDao.java | 56 + .../storage/dao/VMTemplateHostDaoImpl.java | 293 + .../cloud/storage/dao/VMTemplatePoolDao.java | 48 + .../storage/dao/VMTemplatePoolDaoImpl.java | 255 + .../cloud/storage/dao/VMTemplateZoneDao.java | 35 + .../storage/dao/VMTemplateZoneDaoImpl.java | 87 + core/src/com/cloud/storage/dao/VolumeDao.java | 49 + .../com/cloud/storage/dao/VolumeDaoImpl.java | 357 + .../PreallocatedLunDetailVO.java | 62 + .../preallocatedlun/PreallocatedLunVO.java | 131 + .../DummySecondaryStorageResource.java | 163 + .../resource/NfsSecondaryStorageResource.java | 676 + .../storage/resource/StoragePoolResource.java | 43 + .../secondary/SecStorageVmAlertEventArgs.java | 76 + .../SecondaryStorageVmAllocator.java | 28 + .../secondary/SecondaryStorageVmManager.java | 40 + .../storage/template/DownloadManager.java | 112 + .../storage/template/DownloadManagerImpl.java | 904 ++ .../template/HttpTemplateDownloader.java | 450 + .../cloud/storage/template/IsoProcessor.java | 85 + .../template/LocalTemplateDownloader.java | 157 + .../com/cloud/storage/template/Processor.java | 51 + .../storage/template/QCOW2Processor.java | 114 + .../template/ScpTemplateDownloader.java | 163 + .../storage/template/TemplateConstants.java | 40 + .../storage/template/TemplateDownloader.java | 100 + .../template/TemplateDownloaderBase.java | 147 + .../cloud/storage/template/TemplateInfo.java | 78 + .../storage/template/TemplateLocation.java | 215 + .../cloud/storage/template/VhdProcessor.java | 130 + core/src/com/cloud/user/Account.java | 45 + core/src/com/cloud/user/AccountVO.java | 105 + core/src/com/cloud/user/User.java | 72 + core/src/com/cloud/user/UserAccount.java | 57 + core/src/com/cloud/user/UserAccountVO.java | 247 + core/src/com/cloud/user/UserContext.java | 115 + core/src/com/cloud/user/UserStatisticsVO.java | 109 + core/src/com/cloud/user/UserVO.java | 160 + core/src/com/cloud/user/dao/AccountDao.java | 43 + .../com/cloud/user/dao/AccountDaoImpl.java | 183 + .../com/cloud/user/dao/UserAccountDao.java | 28 + .../cloud/user/dao/UserAccountDaoImpl.java | 50 + core/src/com/cloud/user/dao/UserDao.java | 59 + core/src/com/cloud/user/dao/UserDaoImpl.java | 137 + .../com/cloud/user/dao/UserStatisticsDao.java | 35 + .../cloud/user/dao/UserStatisticsDaoImpl.java | 103 + core/src/com/cloud/vm/ConsoleProxy.java | 44 + core/src/com/cloud/vm/ConsoleProxyVO.java | 296 + core/src/com/cloud/vm/DomainRouter.java | 87 + core/src/com/cloud/vm/DomainRouterVO.java | 333 + core/src/com/cloud/vm/SecondaryStorageVm.java | 41 + .../com/cloud/vm/SecondaryStorageVmVO.java | 277 + core/src/com/cloud/vm/UserVmVO.java | 264 + core/src/com/cloud/vm/VMInstanceVO.java | 372 + core/src/com/cloud/vm/VirtualDisk.java | 32 + core/src/com/cloud/vm/VirtualEnvironment.java | 49 + .../com/cloud/vm/VirtualMachineManager.java | 118 + core/src/com/cloud/vm/VirtualMachineName.java | 195 + core/src/com/cloud/vm/VirtualNetwork.java | 84 + core/src/com/cloud/vm/VmStats.java | 27 + .../src/com/cloud/vm/dao/ConsoleProxyDao.java | 51 + .../com/cloud/vm/dao/ConsoleProxyDaoImpl.java | 390 + .../src/com/cloud/vm/dao/DomainRouterDao.java | 117 + .../com/cloud/vm/dao/DomainRouterDaoImpl.java | 299 + .../cloud/vm/dao/SecondaryStorageVmDao.java | 43 + .../vm/dao/SecondaryStorageVmDaoImpl.java | 213 + core/src/com/cloud/vm/dao/UserVmDao.java | 93 + core/src/com/cloud/vm/dao/UserVmDaoImpl.java | 296 + core/src/com/cloud/vm/dao/VMInstanceDao.java | 83 + .../com/cloud/vm/dao/VMInstanceDaoImpl.java | 278 + .../test/com/cloud/async/CleanupDelegate.java | 14 + core/test/com/cloud/async/TestAsync.java | 297 + core/test/com/cloud/vmware/TestVMWare.java | 651 + daemonize/COPYING | 340 + daemonize/daemonize.c | 332 + debian/README | 6 + debian/changelog | 65 + debian/cloud-agent-libs.install | 1 + debian/cloud-agent-scripts.config | 0 debian/cloud-agent-scripts.install | 2 + debian/cloud-agent-scripts.postinst | 13 + debian/cloud-agent.config | 0 debian/cloud-agent.install | 12 + debian/cloud-agent.postinst | 18 + debian/cloud-client-ui.install | 2 + debian/cloud-client.config | 0 debian/cloud-client.install | 33 + debian/cloud-client.postinst | 35 + debian/cloud-console-proxy.config | 0 debian/cloud-console-proxy.install | 9 + debian/cloud-console-proxy.postinst | 18 + debian/cloud-core.install | 2 + debian/cloud-daemonize.install | 2 + debian/cloud-deps.install | 17 + debian/cloud-management.config | 0 debian/cloud-premium-deps.install | 2 + debian/cloud-premium.install | 9 + debian/cloud-python.install | 1 + debian/cloud-server.install | 2 + debian/cloud-setup.install | 14 + debian/cloud-test.install | 6 + debian/cloud-usage.install | 7 + debian/cloud-usage.postinst | 31 + debian/cloud-utils.install | 5 + debian/cloud-vnet.install | 3 + debian/cloud-vnet.postinst | 13 + debian/compat | 1 + debian/control | 184 + debian/copyright | 46 + debian/dirs | 2 + debian/rules | 129 + deps/cloud-apache-log4j-extras-1.0.jar | Bin 0 -> 153863 bytes deps/cloud-backport-util-concurrent-3.0.jar | Bin 0 -> 327810 bytes deps/cloud-cglib.jar | Bin 0 -> 332756 bytes deps/cloud-commons-codec-1.4.jar | Bin 0 -> 58160 bytes deps/cloud-ehcache.jar | Bin 0 -> 263854 bytes deps/cloud-email.jar | Bin 0 -> 434812 bytes deps/cloud-gson-1.3.jar | Bin 0 -> 156797 bytes deps/cloud-httpcore-4.0.jar | Bin 0 -> 172914 bytes deps/cloud-jna.jar | Bin 0 -> 944033 bytes deps/cloud-junit-4.8.1.jar | Bin 0 -> 237047 bytes deps/cloud-libvirt-0.4.5.jar | Bin 0 -> 67513 bytes deps/cloud-log4j.jar | Bin 0 -> 391834 bytes deps/cloud-mysql-connector-java-5.1.7-bin.jar | Bin 0 -> 709922 bytes deps/cloud-trilead-ssh2-build213.jar | Bin 0 -> 252914 bytes deps/cloud-xenserver-5.5.0-1.jar | Bin 0 -> 396151 bytes deps/cloud-xmlrpc-client-3.1.3.jar | Bin 0 -> 58573 bytes deps/cloud-xmlrpc-common-3.1.3.jar | Bin 0 -> 109131 bytes patches/.classpath | 6 + patches/.project | 11 + patches/kvm/etc/dnsmasq.conf | 461 + patches/kvm/etc/haproxy/haproxy.cfg | 26 + patches/kvm/etc/hosts | 2 + patches/kvm/etc/init.d/domr_webserver | 7 + patches/kvm/etc/init.d/seteth1 | 223 + patches/kvm/etc/init.d/vmops | 140 + patches/kvm/etc/rc.local | 10 + patches/kvm/etc/ssh/sshd_config | 128 + patches/kvm/etc/sysconfig/iptables | 33 + patches/kvm/etc/sysconfig/iptables-config | 48 + patches/kvm/etc/sysconfig/iptables-domp | 18 + patches/kvm/etc/sysconfig/iptables-domr | 23 + patches/kvm/etc/sysctl.conf | 27 + patches/kvm/root/edithosts.sh | 51 + patches/kvm/root/reconfigLB.sh | 25 + patches/kvm/root/run_domr_webserver | 17 + patches/kvm/root/send_password_to_domu.sh | 75 + patches/shared/var/www/html/latest/.htaccess | 4 + .../shared/var/www/html/metadata/.htaccess | 1 + .../shared/var/www/html/userdata/.htaccess | 1 + patches/xenserver/etc/init.d/postinit | 110 + patches/xenserver/etc/init.d/seteth1 | 221 + .../xenserver/etc/sysconfig/iptables-config | 48 + .../etc/sysconfig/iptables-consoleproxy | 20 + .../etc/sysconfig/iptables-secstorage | 20 + patches/xenserver/etc/sysctl.conf | 33 + patches/xenserver/root/clearUsageRules.sh | 22 + patches/xenserver/root/edithosts.sh | 50 + patches/xenserver/root/firewall.sh | 204 + patches/xenserver/root/loadbalancer.sh | 167 + python/lib/cloud_PrettyPrint.py | 324 + python/lib/cloud_sxp.py | 763 ++ python/lib/cloud_utils.py | 1135 ++ python/lib/cloud_utils.pyc | Bin 0 -> 46932 bytes scripts/.classpath | 6 + scripts/.project | 17 + scripts/.pydevproject | 7 + scripts/installer/createtmplt.sh | 249 + scripts/installer/installcentos.sh | 56 + scripts/installer/installdomp.sh | 49 + scripts/installer/installrtr.sh | 56 + scripts/installer/run_installer.sh | 22 + scripts/network/domr/call_firewall.sh | 127 + scripts/network/domr/call_loadbalancer.sh | 83 + scripts/network/domr/dhcp_entry.sh | 47 + scripts/network/domr/firewall.sh | 234 + scripts/network/domr/firewall_vlan.sh | 313 + scripts/network/domr/ipassoc.sh | 211 + scripts/network/domr/ipassoc_vlan.sh | 560 + scripts/network/domr/loadbalancer.sh | 213 + scripts/network/domr/loadbalancer_vlan.sh | 275 + scripts/network/domr/save_password_to_domr.sh | 54 + scripts/network/domr/vm_data.sh | 107 + scripts/storage/checkchildren.sh | 34 + scripts/storage/installIso.sh | 112 + scripts/storage/qcow2/cleanupmyvms.sh | 64 + .../storage/qcow2/create_private_template.sh | 98 + scripts/storage/qcow2/createtmplt.sh | 166 + scripts/storage/qcow2/createvm.sh | 296 + scripts/storage/qcow2/delvm.sh | 60 + scripts/storage/qcow2/get_domr_kernel.sh | 113 + scripts/storage/qcow2/get_iqn.sh | 16 + scripts/storage/qcow2/importmpl.sh | 205 + scripts/storage/qcow2/listvmdisk.sh | 68 + scripts/storage/qcow2/listvmdisksize.sh | 70 + scripts/storage/qcow2/listvmtmplt.sh | 46 + scripts/storage/qcow2/managesnapshot.sh | 156 + scripts/storage/qcow2/managevolume.sh | 163 + scripts/storage/qcow2/modifyvlan.sh | 254 + scripts/storage/secondary/createtmplt.sh | 223 + scripts/storage/secondary/installIso.sh | 112 + scripts/storage/secondary/installrtng.sh | 128 + scripts/storage/secondary/listvmtmplt.sh | 46 + .../storage/zfs/iscsi/comstar/createtmplt.sh | 270 + scripts/storage/zfs/iscsi/comstar/createvm.sh | 268 + scripts/storage/zfs/iscsi/comstar/delvm.sh | 203 + .../iscsi/comstar/filebacked/createtmplt.sh | 247 + .../zfs/iscsi/comstar/filebacked/createvm.sh | 253 + .../zfs/iscsi/comstar/filebacked/functions.sh | 35 + .../iscsi/comstar/filebacked/listvmdisk.sh | 114 + .../comstar/filebacked/listvmdisksize.sh | 111 + .../iscsi/comstar/filebacked/migratetmplts.sh | 15 + .../iscsi/comstar/filebacked/upgradevmdisk.sh | 75 + .../storage/zfs/iscsi/comstar/functions.sh | 35 + .../zfs/iscsi/comstar/host_group_destroy.sh | 18 + .../storage/zfs/iscsi/comstar/listvmdisk.sh | 109 + scripts/storage/zfs/iscsi/comstar/lu_info.sh | 38 + scripts/storage/zfs/iscsi/comstar/lu_share.sh | 295 + .../zfs/iscsi/comstar/upgradevmdisk.sh | 79 + .../zfs/iscsi/comstar/view_and_lu_remove.sh | 44 + .../storage/zfs/iscsi/comstar/zfs_destroy.sh | 48 + .../zfs/iscsi/create_private_template.sh | 107 + scripts/storage/zfs/iscsi/createdatadisk.sh | 79 + scripts/storage/zfs/iscsi/createtmplt.sh | 270 + scripts/storage/zfs/iscsi/createvm.sh | 248 + scripts/storage/zfs/iscsi/delvm.sh | 133 + scripts/storage/zfs/iscsi/get_iqn.sh | 51 + scripts/storage/zfs/iscsi/listvmdisk.sh | 101 + scripts/storage/zfs/iscsi/listvmdisksize.sh | 111 + scripts/storage/zfs/iscsi/listvmtmplt.sh | 51 + scripts/storage/zfs/iscsi/lu_info.sh | 4 + scripts/storage/zfs/iscsi/lu_share.sh | 1 + scripts/storage/zfs/iscsi/managesnapshot.sh | 105 + scripts/storage/zfs/iscsi/managevolume.sh | 163 + scripts/storage/zfs/iscsi/showdisks.sh | 99 + scripts/storage/zfs/iscsi/upgradevmdisk.sh | 72 + scripts/storage/zfs/nfs/createvm.sh | 174 + scripts/storage/zfs/nfs/delvm.sh | 81 + scripts/storage/zfs/nfs/listclones.sh | 31 + scripts/storage/zfs/nfs/listvmdisk.sh | 79 + scripts/storage/zfs/nfs/listvmdisksize.sh | 104 + scripts/storage/zfs/nfs/rundomr.sh | 196 + scripts/storage/zfs/nfs/runvm.sh | 304 + scripts/storage/zfs/nfs/stopvm.sh | 109 + scripts/storage/zfs/zfs_mount_recovery.sh | 74 + scripts/util/macgen.py | 14 + scripts/util/qemu-ifup | 4 + scripts/vm/hypervisor/kvm/rundomrpre.sh | 345 + scripts/vm/hypervisor/versions.sh | 24 + scripts/vm/hypervisor/xen/rebootvm.sh | 118 + scripts/vm/hypervisor/xen/rundomp.sh | 171 + scripts/vm/hypervisor/xen/rundomr.sh | 281 + scripts/vm/hypervisor/xen/rundomrpre.sh | 249 + scripts/vm/hypervisor/xen/runvm.sh | 352 + scripts/vm/hypervisor/xen/stopvm.sh | 236 + .../hypervisor/xenserver/check_heartbeat.sh | 58 + scripts/vm/hypervisor/xenserver/find_bond.sh | 111 + .../vm/hypervisor/xenserver/hostvmstats.py | 42 + scripts/vm/hypervisor/xenserver/launch_hb.sh | 30 + .../hypervisor/xenserver/make_migratable.sh | 56 + .../vm/hypervisor/xenserver/networkUsage.sh | 149 + .../vm/hypervisor/xenserver/network_info.sh | 50 + .../vm/hypervisor/xenserver/prepsystemvm.sh | 232 + .../xenserver/setup_heartbeat_sr.sh | 83 + .../vm/hypervisor/xenserver/setup_iscsi.sh | 37 + .../vm/hypervisor/xenserver/setupxenserver.sh | 32 + scripts/vm/hypervisor/xenserver/vmops | 1109 ++ scripts/vm/hypervisor/xenserver/vmopsSnapshot | 1000 ++ .../hypervisor/xenserver/xcpserver/NFSSR.py | 265 + .../hypervisor/xenserver/xcpserver/cleanup.py | 2280 ++++ .../vm/hypervisor/xenserver/xcpserver/nfs.py | 145 + .../vm/hypervisor/xenserver/xcpserver/patch | 41 + .../xenserver/xcpserver/scsiutil.py | 468 + .../vm/hypervisor/xenserver/xenheartbeat.sh | 62 + .../xenserver/xenserver56/ISCSISR.py | 670 + .../xenserver/xenserver56/LUNperVDI.py | 124 + .../hypervisor/xenserver/xenserver56/NFSSR.py | 275 + .../xenserver/xenserver56/cleanup.py | 2280 ++++ .../hypervisor/xenserver/xenserver56/nfs.py | 145 + .../vm/hypervisor/xenserver/xenserver56/patch | 41 + .../xenserver/xenserver56/scsiutil.py | 357 + scripts/vm/network/vnet/bridge.sh | 30 + scripts/vm/network/vnet/createvnet.sh | 125 + scripts/vm/network/vnet/modifyvlan.sh | 170 + scripts/vm/network/vnet/vnetcleanup.sh | 76 + scripts/vm/pingtest.sh | 154 + .../vm/storage/iscsi/comstar/iscsi_common.sh | 168 + scripts/vm/storage/iscsi/comstar/mapiscsi.sh | 30 + .../vm/storage/iscsi/comstar/mountrootdisk.sh | 235 + scripts/vm/storage/iscsi/comstar/mountvm.sh | 469 + scripts/vm/storage/iscsi/get_iqn.sh | 51 + scripts/vm/storage/iscsi/iscsi_common.sh | 157 + scripts/vm/storage/iscsi/iscsikill.sh | 76 + scripts/vm/storage/iscsi/iscsimon.sh | 23 + scripts/vm/storage/iscsi/mapiscsi.sh | 28 + scripts/vm/storage/iscsi/mirror.sh | 259 + scripts/vm/storage/iscsi/mirror_common.sh | 101 + scripts/vm/storage/iscsi/mountdatadisk.sh | 181 + scripts/vm/storage/iscsi/mountrootdisk.sh | 235 + scripts/vm/storage/iscsi/mountvm.sh | 302 + scripts/vm/storage/nfs/mountvm.sh | 142 + server/.classpath | 24 + server/.project | 17 + server/conf/log4j-cloud.xml.in | 101 + server/conf/migration-components.xml | 24 + server/scripts/vmops-fix-mysql-config | 13 + .../com/cloud/agent/manager/AgentAttache.java | 480 + .../cloud/agent/manager/AgentManagerImpl.java | 2053 +++ .../com/cloud/agent/manager/AgentMonitor.java | 217 + .../agent/manager/ConnectedAgentAttache.java | 81 + .../agent/manager/DirectAgentAttache.java | 206 + .../com/cloud/agent/manager/DummyAttache.java | 49 + .../agent/manager/SynchronousListener.java | 135 + .../manager/allocator/HostAllocator.java | 35 + .../agent/manager/allocator/PodAllocator.java | 31 + .../allocator/impl/FirstFitAllocator.java | 382 + .../impl/FirstFitRoutingAllocator.java | 53 + .../allocator/impl/RandomAllocator.java | 104 + .../allocator/impl/RecreateHostAllocator.java | 98 + .../allocator/impl/TestingAllocator.java | 98 + .../impl/UserConcentratedAllocator.java | 367 + .../src/com/cloud/alert/AlertManagerImpl.java | 693 + server/src/com/cloud/api/ApiServer.java | 708 ++ server/src/com/cloud/api/ApiServlet.java | 326 + server/src/com/cloud/api/BaseCmd.java | 891 ++ .../src/com/cloud/api/ServerApiException.java | 49 + .../com/cloud/api/commands/AddConfigCmd.java | 84 + .../com/cloud/api/commands/AddHostCmd.java | 240 + .../api/commands/AddSecondaryStorageCmd.java | 175 + .../AssignPortForwardingServiceCmd.java | 124 + .../commands/AssignToLoadBalancerRuleCmd.java | 121 + .../api/commands/AssociateIPAddrCmd.java | 171 + .../com/cloud/api/commands/AttachIsoCmd.java | 121 + .../cloud/api/commands/AttachVolumeCmd.java | 124 + .../AuthorizeNetworkGroupIngressCmd.java | 267 + .../api/commands/CancelMaintenanceCmd.java | 76 + .../CancelPrimaryStorageMaintenanceCmd.java | 94 + .../com/cloud/api/commands/CopyIsoCmd.java | 132 + .../cloud/api/commands/CopyTemplateCmd.java | 135 + .../api/commands/CreateDiskOfferingCmd.java | 102 + .../cloud/api/commands/CreateDomainCmd.java | 107 + .../commands/CreateIPForwardingRuleCmd.java | 138 + .../commands/CreateLoadBalancerRuleCmd.java | 155 + .../api/commands/CreateNetworkGroupCmd.java | 126 + .../com/cloud/api/commands/CreatePodCmd.java | 107 + .../CreatePortForwardingServiceCmd.java | 123 + .../CreatePortForwardingServiceRuleCmd.java | 117 + .../commands/CreateServiceOfferingCmd.java | 150 + .../cloud/api/commands/CreateSnapshotCmd.java | 185 + .../api/commands/CreateSnapshotPolicyCmd.java | 125 + .../api/commands/CreateStoragePoolCmd.java | 180 + .../cloud/api/commands/CreateTemplateCmd.java | 173 + .../com/cloud/api/commands/CreateUserCmd.java | 124 + .../api/commands/CreateVlanIpRangeCmd.java | 140 + .../cloud/api/commands/CreateVolumeCmd.java | 214 + .../com/cloud/api/commands/CreateZoneCmd.java | 100 + .../api/commands/DeleteDiskOfferingCmd.java | 65 + .../cloud/api/commands/DeleteDomainCmd.java | 86 + .../com/cloud/api/commands/DeleteHostCmd.java | 77 + .../commands/DeleteIPForwardingRuleCmd.java | 107 + .../com/cloud/api/commands/DeleteIsoCmd.java | 108 + .../commands/DeleteLoadBalancerRuleCmd.java | 91 + .../api/commands/DeleteNetworkGroupCmd.java | 102 + .../com/cloud/api/commands/DeletePodCmd.java | 78 + .../com/cloud/api/commands/DeletePoolCmd.java | 65 + .../DeletePortForwardingServiceCmd.java | 85 + .../DeletePortForwardingServiceRuleCmd.java | 81 + .../commands/DeleteServiceOfferingCmd.java | 85 + .../cloud/api/commands/DeleteSnapshotCmd.java | 95 + .../commands/DeleteSnapshotPoliciesCmd.java | 120 + .../cloud/api/commands/DeleteTemplateCmd.java | 109 + .../com/cloud/api/commands/DeleteUserCmd.java | 75 + .../api/commands/DeleteVlanIpRangeCmd.java | 75 + .../cloud/api/commands/DeleteVolumeCmd.java | 94 + .../com/cloud/api/commands/DeleteZoneCmd.java | 78 + .../com/cloud/api/commands/DeployVMCmd.java | 206 + .../api/commands/DestroyConsoleProxyCmd.java | 71 + .../com/cloud/api/commands/DestroyVMCmd.java | 93 + .../com/cloud/api/commands/DetachIsoCmd.java | 104 + .../cloud/api/commands/DetachVolumeCmd.java | 102 + .../cloud/api/commands/DisableAccountCmd.java | 85 + .../cloud/api/commands/DisableUserCmd.java | 88 + .../api/commands/DisassociateIPAddrCmd.java | 100 + .../cloud/api/commands/EnableAccountCmd.java | 86 + .../com/cloud/api/commands/EnableUserCmd.java | 89 + .../api/commands/GetCloudIdentifierCmd.java | 61 + .../cloud/api/commands/ListAccountsCmd.java | 241 + .../com/cloud/api/commands/ListAlertsCmd.java | 103 + .../cloud/api/commands/ListAsyncJobsCmd.java | 123 + .../cloud/api/commands/ListCapacityCmd.java | 217 + .../com/cloud/api/commands/ListCfgsByCmd.java | 104 + .../cloud/api/commands/ListClustersCmd.java | 114 + .../api/commands/ListDiskOfferingsCmd.java | 109 + .../api/commands/ListDomainChildrenCmd.java | 128 + .../cloud/api/commands/ListDomainsCmd.java | 120 + .../com/cloud/api/commands/ListEventsCmd.java | 182 + .../commands/ListGuestOsCategoriesCmd.java | 106 + .../cloud/api/commands/ListGuestOsCmd.java | 114 + .../com/cloud/api/commands/ListHostsCmd.java | 225 + .../api/commands/ListIsoPermissionsCmd.java | 24 + .../com/cloud/api/commands/ListIsosCmd.java | 265 + .../ListLoadBalancerRuleInstancesCmd.java | 113 + .../commands/ListLoadBalancerRulesCmd.java | 173 + .../api/commands/ListNetworkGroupsCmd.java | 181 + .../com/cloud/api/commands/ListPodsByCmd.java | 103 + .../commands/ListPortForwardingRulesCmd.java | 156 + .../ListPortForwardingServiceRulesCmd.java | 129 + .../ListPortForwardingServicesByVmCmd.java | 135 + .../ListPortForwardingServicesCmd.java | 149 + .../api/commands/ListPreallocatedLunsCmd.java | 113 + .../commands/ListPublicIpAddressesCmd.java | 185 + .../ListRecurringSnapshotScheduleCmd.java | 90 + .../api/commands/ListResourceLimitsCmd.java | 175 + .../cloud/api/commands/ListRoutersCmd.java | 189 + .../api/commands/ListServiceOfferingsCmd.java | 135 + .../api/commands/ListSnapshotPoliciesCmd.java | 86 + .../cloud/api/commands/ListSnapshotsCmd.java | 170 + .../commands/ListStoragePoolsAndHostsCmd.java | 86 + .../api/commands/ListStoragePoolsCmd.java | 165 + .../cloud/api/commands/ListSystemVMsCmd.java | 211 + .../ListTemplateOrIsoPermissionsCmd.java | 128 + .../commands/ListTemplatePermissionsCmd.java | 51 + .../cloud/api/commands/ListTemplatesCmd.java | 245 + .../com/cloud/api/commands/ListUsersCmd.java | 138 + .../com/cloud/api/commands/ListVMsCmd.java | 273 + .../api/commands/ListVlanIpRangesCmd.java | 157 + .../cloud/api/commands/ListVolumesCmd.java | 236 + .../cloud/api/commands/ListZonesByCmd.java | 116 + .../cloud/api/commands/LockAccountCmd.java | 75 + .../com/cloud/api/commands/LockUserCmd.java | 78 + .../commands/PrepareForMaintenanceCmd.java | 83 + ...reparePrimaryStorageForMaintenanceCmd.java | 95 + .../api/commands/QueryAsyncJobResultCmd.java | 136 + .../cloud/api/commands/RebootRouterCmd.java | 79 + .../cloud/api/commands/RebootSystemVmCmd.java | 71 + .../com/cloud/api/commands/RebootVMCmd.java | 90 + .../cloud/api/commands/ReconnectHostCmd.java | 75 + .../com/cloud/api/commands/RecoverVMCmd.java | 88 + .../com/cloud/api/commands/RegisterCmd.java | 69 + .../cloud/api/commands/RegisterIsoCmd.java | 198 + .../api/commands/RegisterTemplateCmd.java | 229 + .../RemoveFromLoadBalancerRuleCmd.java | 114 + .../RemovePortForwardingServiceCmd.java | 120 + .../api/commands/ResetVMPasswordCmd.java | 99 + .../RevokeNetworkGroupIngressCmd.java | 225 + .../cloud/api/commands/StartRouterCmd.java | 81 + .../cloud/api/commands/StartSystemVMCmd.java | 76 + .../com/cloud/api/commands/StartVMCmd.java | 99 + .../com/cloud/api/commands/StopRouterCmd.java | 79 + .../cloud/api/commands/StopSystemVmCmd.java | 72 + .../src/com/cloud/api/commands/StopVMCmd.java | 91 + .../cloud/api/commands/UpdateAccountCmd.java | 96 + .../com/cloud/api/commands/UpdateCfgCmd.java | 73 + .../api/commands/UpdateDiskOfferingCmd.java | 85 + .../cloud/api/commands/UpdateDomainCmd.java | 97 + .../com/cloud/api/commands/UpdateHostCmd.java | 203 + .../commands/UpdateIPForwardingRuleCmd.java | 117 + .../com/cloud/api/commands/UpdateIsoCmd.java | 112 + .../api/commands/UpdateIsoPermissionsCmd.java | 24 + .../commands/UpdateLoadBalancerRuleCmd.java | 80 + .../com/cloud/api/commands/UpdatePodCmd.java | 106 + .../api/commands/UpdateResourceLimitCmd.java | 168 + .../commands/UpdateServiceOfferingCmd.java | 105 + .../api/commands/UpdateStoragePoolCmd.java | 119 + .../cloud/api/commands/UpdateTemplateCmd.java | 115 + .../UpdateTemplateOrIsoPermissionsCmd.java | 129 + .../UpdateTemplatePermissionsCmd.java | 42 + .../com/cloud/api/commands/UpdateUserCmd.java | 126 + .../com/cloud/api/commands/UpdateVMCmd.java | 114 + .../com/cloud/api/commands/UpdateZoneCmd.java | 106 + .../com/cloud/api/commands/UpgradeVMCmd.java | 104 + .../src/com/cloud/async/AsyncJobExecutor.java | 44 + .../cloud/async/AsyncJobExecutorContext.java | 54 + .../async/AsyncJobExecutorContextImpl.java | 239 + .../src/com/cloud/async/AsyncJobManager.java | 40 + .../com/cloud/async/AsyncJobManagerImpl.java | 586 + .../com/cloud/async/BaseAsyncJobExecutor.java | 73 + .../src/com/cloud/async/SyncQueueManager.java | 34 + .../com/cloud/async/SyncQueueManagerImpl.java | 242 + .../executor/AssignSecurityGroupExecutor.java | 107 + .../AssignToLoadBalancerExecutor.java | 53 + .../executor/AssociateIpAddressExecutor.java | 137 + .../executor/AssociateIpAddressParam.java | 68 + .../AssociateIpAddressResultObject.java | 113 + .../async/executor/AttachISOExecutor.java | 89 + .../cloud/async/executor/AttachISOParam.java | 61 + .../AttachVolumeOperationResultObject.java | 103 + .../AuthorizeNetworkGroupIngressExecutor.java | 82 + ...ncelPrimaryStorageMaintenanceExecutor.java | 116 + .../executor/CompleteMaintenanceExecutor.java | 188 + .../executor/ConsoleProxyExecutorHelper.java | 54 + .../ConsoleProxyOperationResultObject.java | 260 + .../async/executor/CopyTemplateExecutor.java | 134 + .../async/executor/CopyTemplateParam.java | 64 + .../executor/CopyTemplateResultObject.java | 237 + .../executor/CreateOrUpdateRuleExecutor.java | 117 + .../executor/CreateOrUpdateRuleParam.java | 140 + .../CreateOrUpdateRuleResultObject.java | 123 + .../CreatePrivateTemplateExecutor.java | 218 + .../executor/CreatePrivateTemplateParam.java | 154 + .../CreatePrivateTemplateResultObject.java | 227 + .../executor/CreateSnapshotExecutor.java | 129 + .../executor/CreateSnapshotResultObject.java | 149 + .../CreateVolumeFromSnapshotExecutor.java | 133 + .../async/executor/DeleteDomainExecutor.java | 54 + .../async/executor/DeleteDomainParam.java | 44 + .../executor/DeleteLoadBalancerExecutor.java | 64 + .../DeleteNetworkRuleConfigExecutor.java | 49 + .../async/executor/DeleteRuleExecutor.java | 71 + .../cloud/async/executor/DeleteRuleParam.java | 58 + .../executor/DeleteSecurityGroupExecutor.java | 51 + .../executor/DeleteSnapshotExecutor.java | 80 + .../executor/DeleteTemplateExecutor.java | 71 + .../async/executor/DeleteTemplateParam.java | 58 + .../async/executor/DeleteUserExecutor.java | 57 + .../async/executor/DeployVMExecutor.java | 237 + .../cloud/async/executor/DeployVMParam.java | 169 + .../async/executor/DeployVMResultObject.java | 381 + .../executor/DestroyConsoleProxyExecutor.java | 71 + .../async/executor/DestroyVMExecutor.java | 195 + .../executor/DisableAccountExecutor.java | 162 + .../async/executor/DisableUserExecutor.java | 187 + .../DisassociateIpAddressExecutor.java | 111 + .../executor/DisassociateIpAddressParam.java | 59 + .../async/executor/HostResultObject.java | 442 + .../executor/IngressRuleResultObject.java | 94 + .../async/executor/LoadBalancerParam.java | 70 + .../executor/NetworkGroupIngressParam.java | 56 + .../executor/NetworkGroupResultObject.java | 160 + .../async/executor/OperationResponse.java | 33 + .../executor/PrepareMaintenanceExecutor.java | 190 + ...parePrimaryStorageMaintenanceExecutor.java | 116 + .../executor/PrimaryStorageResultObject.java | 185 + .../executor/RebootConsoleProxyExecutor.java | 77 + .../async/executor/RebootRouterExecutor.java | 80 + .../async/executor/RebootVMExecutor.java | 161 + .../async/executor/ReconnectExecutor.java | 200 + .../executor/RecurringSnapshotParam.java | 70 + .../RemoveFromLoadBalancerExecutor.java | 69 + .../executor/RemoveSecurityGroupExecutor.java | 91 + .../executor/RemoveSecurityGroupParam.java | 68 + .../executor/ResetVMPasswordExecutor.java | 70 + .../async/executor/ResetVMPasswordParam.java | 58 + .../RevokeNetworkGroupIngressExecutor.java | 55 + .../async/executor/RouterExecutorHelper.java | 64 + .../executor/RouterOperationResultObject.java | 300 + .../async/executor/SecurityGroupParam.java | 90 + .../executor/SnapshotOperationParam.java | 118 + .../executor/StartConsoleProxyExecutor.java | 75 + .../executor/StartConsoleProxyResult.java | 260 + .../async/executor/StartRouterExecutor.java | 88 + .../executor/StartRouterResultObject.java | 300 + .../cloud/async/executor/StartVMExecutor.java | 57 + .../executor/StopConsoleProxyExecutor.java | 77 + .../async/executor/StopRouterExecutor.java | 73 + .../cloud/async/executor/StopVMExecutor.java | 152 + .../async/executor/SystemVmCmdExecutor.java | 144 + .../SystemVmOperationResultObject.java | 260 + .../executor/UpdateLoadBalancerParam.java | 45 + .../UpdateLoadBalancerRuleExecutor.java | 76 + .../UpdateLoadBalancerRuleResultObject.java | 105 + .../UpdatePortForwardingRuleExecutor.java | 88 + .../UpdatePortForwardingRuleResultObject.java | 93 + .../async/executor/UpgradeVMExecutor.java | 133 + .../cloud/async/executor/UpgradeVMParam.java | 67 + .../async/executor/VMExecutorHelper.java | 119 + .../async/executor/VMOperationExecutor.java | 28 + .../async/executor/VMOperationListener.java | 118 + .../async/executor/VMOperationParam.java | 101 + .../executor/VMOperationResultObject.java | 367 + .../cloud/async/executor/VmResultObject.java | 282 + .../executor/VolumeOperationExecutor.java | 163 + .../executor/VolumeOperationListener.java | 121 + .../async/executor/VolumeOperationParam.java | 135 + .../executor/VolumeOperationResultObject.java | 344 + .../cluster/DummyClusterManagerImpl.java | 139 + .../src/com/cloud/configuration/Config.java | 296 + .../configuration/ConfigurationManager.java | 239 + .../ConfigurationManagerImpl.java | 1665 +++ .../AgentBasedConsoleProxyManager.java | 360 + ...entBasedStandaloneConsoleProxyManager.java | 98 + .../ConsoleProxyBalanceAllocator.java | 93 + .../consoleproxy/ConsoleProxyManagerImpl.java | 2363 ++++ .../StaticConsoleProxyManager.java | 97 + .../cloud/ha/CheckOnAgentInvestigator.java | 70 + .../cloud/ha/HighAvailabilityManagerImpl.java | 1199 ++ server/src/com/cloud/ha/InvestigatorImpl.java | 257 + server/src/com/cloud/ha/StorageFence.java | 82 + server/src/com/cloud/ha/VmSyncListener.java | 141 + .../com/cloud/ha/XenServerInvestigator.java | 82 + .../discoverer/VmwareServerDiscoverer.java | 40 + .../xen/discoverer/XcpServerDiscoverer.java | 526 + .../src/com/cloud/maid/StackMaidManager.java | 6 + .../com/cloud/maid/StackMaidManagerImpl.java | 108 + .../src/com/cloud/maint/AgentUpgradeVO.java | 62 + .../src/com/cloud/maint/UpgradeManager.java | 49 + .../com/cloud/maint/UpgradeManagerImpl.java | 210 + .../com/cloud/maint/UpgradeManagerMBean.java | 24 + .../src/com/cloud/maint/UpgradeMonitor.java | 36 + server/src/com/cloud/maint/Version.java | 54 + .../com/cloud/maint/dao/AgentUpgradeDao.java | 24 + .../cloud/maint/dao/AgentUpgradeDaoImpl.java | 27 + .../migration/Db20to21MigrationUtil.java | 933 ++ .../migration/Db21to22MigrationUtil.java | 80 + .../cloud/migration/DiskOffering20Dao.java | 6 + .../migration/DiskOffering20DaoImpl.java | 8 + .../com/cloud/migration/DiskOffering20VO.java | 96 + .../cloud/migration/DiskOffering21Dao.java | 7 + .../migration/DiskOffering21DaoImpl.java | 9 + .../com/cloud/migration/DiskOffering21VO.java | 229 + .../cloud/migration/ServiceOffering20Dao.java | 6 + .../migration/ServiceOffering20DaoImpl.java | 9 + .../cloud/migration/ServiceOffering20VO.java | 181 + .../cloud/migration/ServiceOffering21Dao.java | 6 + .../migration/ServiceOffering21DaoImpl.java | 9 + .../cloud/migration/ServiceOffering21VO.java | 126 + .../src/com/cloud/network/NetworkManager.java | 211 + .../com/cloud/network/NetworkManagerImpl.java | 2348 ++++ .../com/cloud/network/NetworkProfileVO.java | 123 + .../cloud/network/SshKeysDistriMonitor.java | 118 + .../cloud/network/dao/NetworkProfileDao.java | 25 + .../network/dao/NetworkProfileDaoImpl.java | 29 + .../security/NetworkGroupListener.java | 138 + .../network/security/NetworkGroupManager.java | 71 + .../security/NetworkGroupManagerImpl.java | 919 ++ server/src/com/cloud/resource/Discoverer.java | 44 + .../com/cloud/resource/DiscovererBase.java | 63 + .../cloud/resource/DummyHostDiscoverer.java | 96 + .../resource/DummyHostServerResource.java | 165 + .../com/cloud/server/ConfigurationServer.java | 36 + .../cloud/server/ConfigurationServerImpl.java | 514 + .../cloud/server/ManagementServerImpl.java | 8548 +++++++++++++ .../src/com/cloud/server/StatsCollector.java | 444 + .../server/auth/DefaultUserAuthenticator.java | 63 + .../server/auth/MD5UserAuthenticator.java | 98 + .../cloud/server/auth/UserAuthenticator.java | 40 + .../cloud/servlet/CloudStartupServlet.java | 63 + .../cloud/servlet/ConsoleProxyServlet.java | 324 + .../storage/LocalStoragePoolListener.java | 139 + .../com/cloud/storage/StorageManagerImpl.java | 2188 ++++ .../AbstractStoragePoolAllocator.java | 239 + .../FirstFitStoragePoolAllocator.java | 83 + ...GarbageCollectingStoragePoolAllocator.java | 128 + .../allocator/LocalStoragePoolAllocator.java | 300 + .../allocator/RandomStoragePoolAllocator.java | 77 + .../allocator/StoragePoolAllocator.java | 42 + .../allocator/UseLocalForRootAllocator.java | 84 + .../download/DownloadAbandonedState.java | 48 + .../storage/download/DownloadActiveState.java | 111 + .../download/DownloadCompleteState.java | 50 + .../storage/download/DownloadErrorState.java | 91 + .../download/DownloadInProgressState.java | 45 + .../download/DownloadInactiveState.java | 59 + .../storage/download/DownloadListener.java | 383 + .../storage/download/DownloadMonitor.java | 46 + .../storage/download/DownloadMonitorImpl.java | 519 + .../cloud/storage/download/DownloadState.java | 89 + .../storage/download/NotDownloadedState.java | 51 + .../storage/listener/StoragePoolMonitor.java | 107 + .../storage/listener/StorageSyncListener.java | 85 + .../storage/monitor/StorageHostMonitor.java | 32 + .../dao/PreallocatedLunDao.java | 54 + .../dao/PreallocatedLunDaoImpl.java | 237 + .../dao/PreallocatedLunDetailsDao.java | 24 + .../dao/PreallocatedLunDetailsDaoImpl.java | 31 + .../LocalSecondaryStorageResource.java | 183 + .../secondary/SecondaryStorageDiscoverer.java | 285 + .../secondary/SecondaryStorageListener.java | 109 + .../SecondaryStorageManagerImpl.java | 1886 +++ .../SecondaryStorageVmDefaultAllocator.java | 70 + .../storage/snapshot/SnapshotManager.java | 158 + .../storage/snapshot/SnapshotManagerImpl.java | 1346 ++ .../storage/snapshot/SnapshotScheduler.java | 50 + .../snapshot/SnapshotSchedulerImpl.java | 413 + .../com/cloud/template/TemplateManager.java | 131 + .../cloud/template/TemplateManagerImpl.java | 636 + server/src/com/cloud/test/DatabaseConfig.java | 1206 ++ server/src/com/cloud/test/IPRangeConfig.java | 576 + server/src/com/cloud/test/PodZoneConfig.java | 419 + server/src/com/cloud/user/AccountManager.java | 103 + .../com/cloud/user/AccountManagerImpl.java | 332 + server/src/com/cloud/vm/MauriceMoss.java | 67 + server/src/com/cloud/vm/NicVO.java | 106 + server/src/com/cloud/vm/UserVmManager.java | 223 + .../src/com/cloud/vm/UserVmManagerImpl.java | 2942 +++++ server/src/com/cloud/vm/VmManager.java | 42 + server/test/async-job-component.xml | 181 + .../com/cloud/async/TestAsyncJobManager.java | 256 + .../com/cloud/async/TestSyncQueueManager.java | 206 + server/test/sync-queue-component.xml | 8 + setup/.project | 11 + setup/bindir/cloud-migrate-databases.in | 252 + setup/bindir/cloud-setup-databases.in | 354 + setup/db/create-database.sql | 49 + setup/db/create-index-fk.sql | 252 + setup/db/create-schema.sql | 1045 ++ setup/db/data-20to21.sql | 21 + setup/db/deploy-db-dev.sh | 101 + setup/db/index-20to21.sql | 43 + setup/db/migration/schema-21to22.sql | 9 + setup/db/postprocess-20to21.sql | 18 + setup/db/schema-20to21.sql | 184 + setup/db/server-setup.sql | 11 + setup/db/server-setup.xml | 390 + setup/db/templates.kvm.sql | 53 + setup/db/templates.xenserver.sql | 74 + tools/.classpath | 42 + tools/.project | 17 + tools/ant/apache-ant-1.7.1/LICENSE | 272 + tools/ant/apache-ant-1.7.1/bin/ant | 326 + tools/ant/apache-ant-1.7.1/bin/ant.bat | 226 + tools/ant/apache-ant-1.7.1/bin/ant.cmd | 93 + tools/ant/apache-ant-1.7.1/bin/antRun | 24 + tools/ant/apache-ant-1.7.1/bin/antRun.bat | 50 + tools/ant/apache-ant-1.7.1/bin/antRun.pl | 66 + tools/ant/apache-ant-1.7.1/bin/antenv.cmd | 98 + .../apache-ant-1.7.1/bin/complete-ant-cmd.pl | 114 + tools/ant/apache-ant-1.7.1/bin/envset.cmd | 131 + tools/ant/apache-ant-1.7.1/bin/lcp.bat | 31 + tools/ant/apache-ant-1.7.1/bin/runant.pl | 153 + tools/ant/apache-ant-1.7.1/bin/runant.py | 102 + tools/ant/apache-ant-1.7.1/bin/runrc.cmd | 60 + .../apache-ant-1.7.1/etc/ant-bootstrap.jar | Bin 0 -> 18877 bytes tools/ant/apache-ant-1.7.1/lib/README | 16 + tools/ant/apache-ant-1.7.1/lib/ant-antlr.jar | Bin 0 -> 5752 bytes .../apache-ant-1.7.1/lib/ant-apache-bcel.jar | Bin 0 -> 8611 bytes .../apache-ant-1.7.1/lib/ant-apache-bsf.jar | Bin 0 -> 3939 bytes .../apache-ant-1.7.1/lib/ant-apache-log4j.jar | Bin 0 -> 3056 bytes .../apache-ant-1.7.1/lib/ant-apache-oro.jar | Bin 0 -> 39627 bytes .../lib/ant-apache-regexp.jar | Bin 0 -> 3762 bytes .../lib/ant-apache-resolver.jar | Bin 0 -> 4071 bytes .../lib/ant-commons-logging.jar | Bin 0 -> 3910 bytes .../apache-ant-1.7.1/lib/ant-commons-net.jar | Bin 0 -> 47026 bytes tools/ant/apache-ant-1.7.1/lib/ant-jai.jar | Bin 0 -> 21348 bytes .../ant/apache-ant-1.7.1/lib/ant-javamail.jar | Bin 0 -> 6998 bytes .../ant/apache-ant-1.7.1/lib/ant-jdepend.jar | Bin 0 -> 8132 bytes tools/ant/apache-ant-1.7.1/lib/ant-jmf.jar | Bin 0 -> 6593 bytes tools/ant/apache-ant-1.7.1/lib/ant-jsch.jar | Bin 0 -> 30797 bytes tools/ant/apache-ant-1.7.1/lib/ant-junit.jar | Bin 0 -> 93518 bytes .../ant/apache-ant-1.7.1/lib/ant-launcher.jar | Bin 0 -> 12143 bytes .../ant/apache-ant-1.7.1/lib/ant-netrexx.jar | Bin 0 -> 9881 bytes tools/ant/apache-ant-1.7.1/lib/ant-nodeps.jar | Bin 0 -> 431580 bytes .../ant/apache-ant-1.7.1/lib/ant-starteam.jar | Bin 0 -> 35355 bytes .../apache-ant-1.7.1/lib/ant-stylebook.jar | Bin 0 -> 2330 bytes tools/ant/apache-ant-1.7.1/lib/ant-swing.jar | Bin 0 -> 6738 bytes .../ant/apache-ant-1.7.1/lib/ant-testutil.jar | Bin 0 -> 14941 bytes tools/ant/apache-ant-1.7.1/lib/ant-trax.jar | Bin 0 -> 6881 bytes .../ant/apache-ant-1.7.1/lib/ant-weblogic.jar | Bin 0 -> 14205 bytes tools/ant/apache-ant-1.7.1/lib/ant.jar | Bin 0 -> 1323005 bytes .../apache-ant-1.7.1/lib/libraries.properties | 62 + tools/ant/apache-ant-1.7.1/lib/xercesImpl.jar | Bin 0 -> 1223877 bytes tools/ant/apache-ant-1.7.1/lib/xml-apis.jar | Bin 0 -> 194354 bytes tools/gcc/README | 261 + tools/gcc/compiler.jar | Bin 0 -> 4212432 bytes tools/gcc/gcc.sh | 26 + ui/.classpath | 5 + ui/.project | 17 + ui/content/tab_accounts.html | 211 + ui/content/tab_configuration.html | 970 ++ ui/content/tab_dashboard.html | 535 + ui/content/tab_events.html | 376 + ui/content/tab_hosts.html | 316 + ui/content/tab_instances.html | 1710 +++ ui/content/tab_networking.html | 886 ++ ui/content/tab_storage.html | 1524 +++ ui/content/tab_templates.html | 643 + ui/css/cloud_custom.css | 441 + .../images/._ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 4096 bytes .../images/._ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 4096 bytes .../images/._ui-bg_glass_30_393939_1x400.png | Bin 0 -> 47996 bytes .../images/._ui-bg_glass_30_ffffff_1x400.png | Bin 0 -> 4096 bytes .../images/._ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 4096 bytes .../._ui-bg_inset-soft_95_fef1ec_1x100.png | Bin 0 -> 4096 bytes ui/css/images/._ui-icons_222222_256x240.png | Bin 0 -> 4096 bytes ui/css/images/._ui-icons_2e83ff_256x240.png | Bin 0 -> 4096 bytes ui/css/images/._ui-icons_FFF_256x240.png | Bin 0 -> 4096 bytes ui/css/images/._ui-icons_cd0a0a_256x240.png | Bin 0 -> 4096 bytes ui/css/images/._ui-icons_ffffff_256x240.png | Bin 0 -> 4096 bytes ui/css/images/alert_icon.png | Bin 0 -> 545 bytes ui/css/images/clr_button.gif | Bin 0 -> 1274 bytes ui/css/images/clr_button_hover.gif | Bin 0 -> 437 bytes ui/css/images/comment_loader.gif | Bin 0 -> 1849 bytes ui/css/images/grid_headerbg.gif | Bin 0 -> 196 bytes ui/css/images/groupbox_top.gif | Bin 0 -> 613 bytes ui/css/images/minimize_button.gif | Bin 0 -> 634 bytes ui/css/images/minimize_button_hover.gif | Bin 0 -> 227 bytes ui/css/images/play_button.gif | Bin 0 -> 657 bytes ui/css/images/play_button_hover.gif | Bin 0 -> 243 bytes ui/css/images/shrink_button.gif | Bin 0 -> 655 bytes ui/css/images/shrink_button_hover.gif | Bin 0 -> 243 bytes ui/css/images/sortable_groupbox.gif | Bin 0 -> 2773 bytes ui/css/images/sortable_leftarrow.gif | Bin 0 -> 426 bytes ui/css/images/sortable_leftarrow_hover.gif | Bin 0 -> 427 bytes ui/css/images/sortable_rightarrow.gif | Bin 0 -> 425 bytes ui/css/images/sortable_rightarrow_hover.gif | Bin 0 -> 427 bytes ui/css/images/stop_button.gif | Bin 0 -> 649 bytes ui/css/images/stop_button_hover.gif | Bin 0 -> 231 bytes .../ui-bg_errorglass_30_ffffff_1x400.png | Bin 0 -> 1067 bytes ui/css/images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 180 bytes ui/css/images/ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 178 bytes ui/css/images/ui-bg_glass_30_393939_1x400.png | Bin 0 -> 2830 bytes ui/css/images/ui-bg_glass_30_ffffff_1x400.png | Bin 0 -> 105 bytes ui/css/images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 120 bytes ui/css/images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 105 bytes ui/css/images/ui-bg_glass_75_000000_1x400.png | Bin 0 -> 132 bytes ui/css/images/ui-bg_glass_75_212121_1x400.png | Bin 0 -> 132 bytes .../ui-bg_inset-soft_95_fef1ec_1x100.png | Bin 0 -> 123 bytes ui/css/images/ui-icons_222222_256x240.png | Bin 0 -> 4369 bytes ui/css/images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4369 bytes ui/css/images/ui-icons_454545_256x240.png | Bin 0 -> 4369 bytes ui/css/images/ui-icons_FFF_256x240.png | Bin 0 -> 550 bytes ui/css/images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4369 bytes ui/css/images/ui-icons_ffffff_256x240.png | Bin 0 -> 4369 bytes ui/css/jquery-ui-1.7.2.custom.css | 510 + ui/css/jquery-ui-1.8.2.custom.css | 566 + ui/css/logger.css | 119 + ui/css/main.css | 7906 ++++++++++++ ui/favicon.ico | Bin 0 -> 1406 bytes ui/images/32bit_icon.gif | Bin 0 -> 856 bytes ui/images/64bit_icon.gif | Bin 0 -> 856 bytes ui/images/ISO_OFF.gif | Bin 0 -> 1185 bytes ui/images/ISO_ON.gif | Bin 0 -> 1170 bytes ui/images/KVM_icon.gif | Bin 0 -> 1446 bytes ui/images/XEN_icon.gif | Bin 0 -> 1004 bytes ui/images/account_bg.gif | Bin 0 -> 310 bytes ui/images/account_shadow.gif | Bin 0 -> 635 bytes ui/images/accountstitle_icons.gif | Bin 0 -> 1468 bytes ui/images/add_domainbutton.gif | Bin 0 -> 1451 bytes ui/images/add_domainbutton_hover.gif | Bin 0 -> 1536 bytes ui/images/add_ipbutton.gif | Bin 0 -> 2154 bytes ui/images/add_ipbutton_hover.gif | Bin 0 -> 2185 bytes ui/images/add_userbutton.gif | Bin 0 -> 1468 bytes ui/images/add_userbutton_hover.gif | Bin 0 -> 1540 bytes ui/images/addnetgroup_button.gif | Bin 0 -> 1885 bytes ui/images/addnetgroup_button_hover.gif | Bin 0 -> 1957 bytes ui/images/admin_vmblue_left.gif | Bin 0 -> 487 bytes ui/images/admin_vmblue_mid.gif | Bin 0 -> 464 bytes ui/images/admin_vmblue_right.gif | Bin 0 -> 481 bytes ui/images/admin_vmconsole.gif | Bin 0 -> 1541 bytes ui/images/admin_vmconsole_hover.gif | Bin 0 -> 1556 bytes ui/images/admin_vmgrey_left.gif | Bin 0 -> 454 bytes ui/images/admin_vmgrey_mid.gif | Bin 0 -> 300 bytes ui/images/admin_vmgrey_right.gif | Bin 0 -> 453 bytes ui/images/admin_vmyellow_left.gif | Bin 0 -> 453 bytes ui/images/admin_vmyellow_mid.gif | Bin 0 -> 292 bytes ui/images/admin_vmyellow_right.gif | Bin 0 -> 450 bytes ui/images/adv_searchbutton.gif | Bin 0 -> 781 bytes ui/images/adv_searchbutton_hover.gif | Bin 0 -> 781 bytes ui/images/ajax-loader.gif | Bin 0 -> 1849 bytes ui/images/alert_icon.png | Bin 0 -> 545 bytes ui/images/alerttitle_icons.gif | Bin 0 -> 1567 bytes ui/images/alpha_rowbg.png | Bin 0 -> 143 bytes ui/images/alpha_vmrowbg.png | Bin 0 -> 156 bytes ui/images/arrow_bullet.gif | Bin 0 -> 49 bytes ui/images/bigrotation2.gif | Bin 0 -> 1787 bytes ui/images/bootable_nonselectedicon.gif | Bin 0 -> 1034 bytes ui/images/bootable_selectedicon.gif | Bin 0 -> 997 bytes ui/images/box_bullet.gif | Bin 0 -> 193 bytes ui/images/bytes_in.gif | Bin 0 -> 1058 bytes ui/images/bytes_out.gif | Bin 0 -> 1023 bytes ui/images/calendar_icon.gif | Bin 0 -> 1062 bytes ui/images/capacitypanel_bot.gif | Bin 0 -> 12878 bytes ui/images/capacitypanel_mid.gif | Bin 0 -> 1291 bytes ui/images/capacitypanel_top.gif | Bin 0 -> 15212 bytes ui/images/close_button.png | Bin 0 -> 1450 bytes ui/images/close_button_hover.png | Bin 0 -> 2034 bytes ui/images/cloud_logo.gif | Bin 0 -> 8973 bytes ui/images/console_bg.png | Bin 0 -> 4868 bytes ui/images/cproxytitle_icons.gif | Bin 0 -> 1609 bytes ui/images/cpu_icon.gif | Bin 0 -> 1095 bytes ui/images/crosszone_nonselectedicon.gif | Bin 0 -> 1056 bytes ui/images/crosszone_selectedicon.gif | Bin 0 -> 1072 bytes ui/images/dashboardtitle_icons.gif | Bin 0 -> 1677 bytes ui/images/db_bardg_bg.gif | Bin 0 -> 1202 bytes ui/images/db_bardg_titlebg.gif | Bin 0 -> 697 bytes ui/images/db_domain_top.gif | Bin 0 -> 6638 bytes ui/images/db_domainbg.gif | Bin 0 -> 955 bytes ui/images/db_vmmid.gif | Bin 0 -> 204 bytes ui/images/db_vmyellowmid.gif | Bin 0 -> 387 bytes ui/images/details_downarrow.jpg | Bin 0 -> 158 bytes ui/images/details_uparrow.jpg | Bin 0 -> 158 bytes ui/images/disk_icon.gif | Bin 0 -> 582 bytes ui/images/diskofftitle_icons.gif | Bin 0 -> 1416 bytes ui/images/display_boxbot.gif | Bin 0 -> 5690 bytes ui/images/display_boxmid.gif | Bin 0 -> 321 bytes ui/images/display_boxtop.gif | Bin 0 -> 4066 bytes ui/images/display_deleteicon.png | Bin 0 -> 166 bytes ui/images/display_deleteicon_hover.png | Bin 0 -> 166 bytes ui/images/display_editicon.png | Bin 0 -> 286 bytes ui/images/display_editicon_hover.png | Bin 0 -> 282 bytes ui/images/display_headerbg.gif | Bin 0 -> 206 bytes ui/images/display_rollbackicon.png | Bin 0 -> 341 bytes ui/images/display_rollbackicon_hover.png | Bin 0 -> 341 bytes ui/images/displaygrid_loader.gif | Bin 0 -> 1849 bytes ui/images/domain_dbaccount.gif | Bin 0 -> 2686 bytes ui/images/domain_dbdiskoff.gif | Bin 0 -> 4467 bytes ui/images/domain_dbinstance.gif | Bin 0 -> 3526 bytes ui/images/domain_dbsnapshots.gif | Bin 0 -> 3693 bytes ui/images/domain_serachboxleft.gif | Bin 0 -> 2274 bytes ui/images/domain_serachboxmid.gif | Bin 0 -> 508 bytes ui/images/domain_serachboxright.gif | Bin 0 -> 2492 bytes ui/images/domaintitle_icons.gif | Bin 0 -> 1443 bytes ui/images/domdetailsbox_left.gif | Bin 0 -> 3482 bytes ui/images/domdetailsbox_mid.gif | Bin 0 -> 392 bytes ui/images/domdetailsbox_right.gif | Bin 0 -> 2769 bytes ui/images/eventstitle_icons.gif | Bin 0 -> 1546 bytes ui/images/featured_nonselectedicon.gif | Bin 0 -> 950 bytes ui/images/featured_selectedicon.gif | Bin 0 -> 967 bytes ui/images/graph_titleicon.gif | Bin 0 -> 1200 bytes ui/images/green_statusbar.gif | Bin 0 -> 536 bytes ui/images/grey_statusbar.gif | Bin 0 -> 326 bytes ui/images/grid_headerbg | Bin 0 -> 224 bytes ui/images/grid_headerbg.gif | Bin 0 -> 204 bytes ui/images/grid_loader.gif | Bin 0 -> 1849 bytes ui/images/grid_morebutton.gif | Bin 0 -> 1427 bytes ui/images/grid_morebutton_hover.gif | Bin 0 -> 1427 bytes ui/images/gridresult_closebutton.gif | Bin 0 -> 1230 bytes ui/images/gridresult_closebutton_hover.gif | Bin 0 -> 1231 bytes ui/images/gridsorting_downarrow.gif | Bin 0 -> 51 bytes ui/images/gridsorting_uparrow.gif | Bin 0 -> 51 bytes ui/images/group_icon.gif | Bin 0 -> 634 bytes ui/images/gsettingstitle_icons.gif | Bin 0 -> 1543 bytes ui/images/ha_disable.gif | Bin 0 -> 785 bytes ui/images/ha_enable.gif | Bin 0 -> 803 bytes ui/images/header_bg | Bin 0 -> 491 bytes ui/images/header_bg.gif | Bin 0 -> 519 bytes ui/images/hostdetails_headerbg.jpg | Bin 0 -> 410 bytes ui/images/hostnetwork_icon.gif | Bin 0 -> 722 bytes ui/images/hosttitle_icons.gif | Bin 0 -> 1641 bytes ui/images/hvm_nonselectedicon.gif | Bin 0 -> 839 bytes ui/images/hvm_selectedicon.gif | Bin 0 -> 858 bytes ui/images/instancetitle_icons.gif | Bin 0 -> 1654 bytes ui/images/ip_ORicon.gif | Bin 0 -> 749 bytes ui/images/ip_detailtopbox.gif | Bin 0 -> 8412 bytes ui/images/ip_managebox_bg.gif | Bin 0 -> 175 bytes ui/images/ip_managebox_icon.gif | Bin 0 -> 770 bytes ui/images/ip_searchbutton.gif | Bin 0 -> 767 bytes ui/images/ipbox_nonselected.gif | Bin 0 -> 941 bytes ui/images/ipbox_selected.gif | Bin 0 -> 2313 bytes ui/images/ipdescr_boxbot.gif | Bin 0 -> 16820 bytes ui/images/ipdescr_boxmid.gif | Bin 0 -> 6626 bytes ui/images/ipdescr_boxtop.gif | Bin 0 -> 10762 bytes ui/images/ipdescr_contbot.gif | Bin 0 -> 2148 bytes ui/images/ipdescr_contbot_blank.gif | Bin 0 -> 2345 bytes ui/images/ipdescr_contmid.gif | Bin 0 -> 824 bytes ui/images/ipdescr_contmid_blank.gif | Bin 0 -> 1227 bytes ui/images/ipdescr_conttop.gif | Bin 0 -> 5976 bytes ui/images/ipdescr_conttop_blank.gif | Bin 0 -> 5756 bytes ui/images/iptitle_icons.gif | Bin 0 -> 1599 bytes ui/images/isotitle_icons.gif | Bin 0 -> 1671 bytes ui/images/laoding.gif | Bin 0 -> 14647 bytes ui/images/load_add.gif | Bin 0 -> 870 bytes ui/images/load_addicon.gif | Bin 0 -> 354 bytes ui/images/load_aniloader.gif | Bin 0 -> 1849 bytes ui/images/load_joint.gif | Bin 0 -> 59 bytes ui/images/load_routericon.gif | Bin 0 -> 3674 bytes ui/images/load_stoppedvm.gif | Bin 0 -> 3650 bytes ui/images/load_workingvm.gif | Bin 0 -> 2723 bytes ui/images/loadgr_closebutton.png | Bin 0 -> 474 bytes ui/images/loadgr_closebutton_hover.png | Bin 0 -> 487 bytes ui/images/loadind_textbg.gif | Bin 0 -> 407 bytes ui/images/loading.gif | Bin 0 -> 2718 bytes ui/images/loading_1.gif | Bin 0 -> 2704 bytes ui/images/loading_addbg.gif | Bin 0 -> 870 bytes ui/images/loading_load.gif | Bin 0 -> 870 bytes ui/images/loading_messagebg.gif | Bin 0 -> 225 bytes ui/images/loadingmsg_left.gif | Bin 0 -> 609 bytes ui/images/loadingmsg_mid.gif | Bin 0 -> 776 bytes ui/images/loadingmsg_right.gif | Bin 0 -> 657 bytes ui/images/loadnetwork_titleicon.gif | Bin 0 -> 777 bytes ui/images/loadtitle_icons.gif | Bin 0 -> 1507 bytes ui/images/loadvm_loader.gif | Bin 0 -> 1849 bytes ui/images/logo.gif | Bin 0 -> 11559 bytes ui/images/logout_bg.gif | Bin 0 -> 547 bytes ui/images/logout_bot.gif | Bin 0 -> 7037 bytes ui/images/logout_cloudlogo.gif | Bin 0 -> 4743 bytes ui/images/logout_logo.gif | Bin 0 -> 4291 bytes ui/images/logout_mid.gif | Bin 0 -> 570 bytes ui/images/logout_top.gif | Bin 0 -> 46277 bytes ui/images/mcontent_bg.gif | Bin 0 -> 179 bytes ui/images/memory_icon.gif | Bin 0 -> 1159 bytes ui/images/overlay_morebot.png | Bin 0 -> 1346 bytes ui/images/overlay_moremid.png | Bin 0 -> 314 bytes ui/images/overlay_moretop.png | Bin 0 -> 970 bytes ui/images/pagination_bg.gif | Bin 0 -> 108 bytes ui/images/pagination_firsticon.gif | Bin 0 -> 636 bytes ui/images/pagination_lasticon.gif | Bin 0 -> 636 bytes ui/images/pagination_nexticon.gif | Bin 0 -> 615 bytes ui/images/pagination_previcon.gif | Bin 0 -> 618 bytes ui/images/pagination_refresh.gif | Bin 0 -> 1147 bytes ui/images/password_nonselectedicon.gif | Bin 0 -> 1014 bytes ui/images/password_selectedicon.gif | Bin 0 -> 1447 bytes ui/images/pin_icon.gif | Bin 0 -> 601 bytes ui/images/portnetwork_titleicon.gif | Bin 0 -> 805 bytes ui/images/primestoragetitle_icons.gif | Bin 0 -> 1704 bytes ui/images/public_nonselectedicon.gif | Bin 0 -> 906 bytes ui/images/public_selectedicon.gif | Bin 0 -> 901 bytes ui/images/red_statusbar.gif | Bin 0 -> 535 bytes ui/images/register_box.gif | Bin 0 -> 2016 bytes ui/images/rev_wizbot.gif | Bin 0 -> 661 bytes ui/images/rev_wizmid.gif | Bin 0 -> 149 bytes ui/images/rev_wiztop.jpg | Bin 0 -> 22700 bytes ui/images/revwiz_backbutton.gif | Bin 0 -> 1383 bytes ui/images/revwiz_closebutton.gif | Bin 0 -> 1295 bytes ui/images/revwiz_closebutton_hover.gif | Bin 0 -> 1297 bytes ui/images/revwiz_nextbutton.gif | Bin 0 -> 2733 bytes ui/images/revwiz_nonselcted_tempbut.gif | Bin 0 -> 2278 bytes ui/images/revwiz_nonselcted_tempbut_hover.gif | Bin 0 -> 4481 bytes ui/images/revwiz_nonselectednumber.gif | Bin 0 -> 2179 bytes ui/images/revwiz_selcted_tempbut.gif | Bin 0 -> 5065 bytes ui/images/revwiz_selectednumber.gif | Bin 0 -> 2187 bytes ui/images/routerstitle_icons.gif | Bin 0 -> 1582 bytes ui/images/running_icon.gif | Bin 0 -> 588 bytes ui/images/safebar_bg.gif | Bin 0 -> 197 bytes ui/images/search_closearrow.gif | Bin 0 -> 203 bytes ui/images/search_resulticon.gif | Bin 0 -> 1166 bytes ui/images/searchicon_button.jpg | Bin 0 -> 1460 bytes ui/images/searchicon_button_hover.jpg | Bin 0 -> 1486 bytes ui/images/searchpanel_bg.gif | Bin 0 -> 207 bytes ui/images/secondstoragetitle_icons.gif | Bin 0 -> 1699 bytes ui/images/select_ipbg.gif | Bin 0 -> 7263 bytes ui/images/select_ipbg_admin.gif | Bin 0 -> 11051 bytes ui/images/serviceofftitle_icons.gif | Bin 0 -> 1451 bytes ui/images/sgtitle_icons.gif | Bin 0 -> 1418 bytes ui/images/small_ppbot.png | Bin 0 -> 2866 bytes ui/images/small_ppmid.png | Bin 0 -> 602 bytes ui/images/small_pptop.png | Bin 0 -> 2794 bytes ui/images/smenu_selectedbg.gif | Bin 0 -> 235 bytes ui/images/sprint.gif | Bin 0 -> 17003 bytes ui/images/sprite1.gif | Bin 0 -> 65239 bytes ui/images/srow_loading.png | Bin 0 -> 120 bytes ui/images/step1.png | Bin 0 -> 1190 bytes ui/images/step2.png | Bin 0 -> 1483 bytes ui/images/step3.png | Bin 0 -> 1537 bytes ui/images/step4.png | Bin 0 -> 1395 bytes ui/images/stepbox_slected.gif | Bin 0 -> 253 bytes ui/images/stopped_icon.gif | Bin 0 -> 550 bytes ui/images/stopped_vm.gif | Bin 0 -> 3400 bytes ui/images/storagetitle_icons.gif | Bin 0 -> 1777 bytes ui/images/submenu_bg.gif | Bin 0 -> 144 bytes ui/images/submenu_linkbg.gif | Bin 0 -> 2212 bytes ui/images/temp_centosicon.gif | Bin 0 -> 1024 bytes ui/images/temp_deleteicon.gif | Bin 0 -> 562 bytes ui/images/temp_deleteicon_hover.gif | Bin 0 -> 562 bytes ui/images/temp_editicon.gif | Bin 0 -> 551 bytes ui/images/temp_editicon_hover.gif | Bin 0 -> 550 bytes ui/images/temp_linuxicon.gif | Bin 0 -> 552 bytes ui/images/temp_windowsicon.gif | Bin 0 -> 1083 bytes ui/images/templatestitle_icons.gif | Bin 0 -> 1527 bytes ui/images/test_icon.gif | Bin 0 -> 1048 bytes ui/images/tick_arrow.gif | Bin 0 -> 372 bytes ui/images/total_vm.gif | Bin 0 -> 3066 bytes ui/images/tree_boxright.gif | Bin 0 -> 3219 bytes ui/images/tree_eventicon.gif | Bin 0 -> 581 bytes ui/images/tree_minusopen_icon.png | Bin 0 -> 728 bytes ui/images/trusted_icon.gif | Bin 0 -> 916 bytes ui/images/unknown_icon.gif | Bin 0 -> 885 bytes ui/images/unsafebar_bg.gif | Bin 0 -> 160 bytes ui/images/untrusted_icon.gif | Bin 0 -> 955 bytes ui/images/userlinks_bgleft.gif | Bin 0 -> 640 bytes ui/images/userlinks_bgmid.gif | Bin 0 -> 285 bytes ui/images/userlinks_bgright.gif | Bin 0 -> 639 bytes ui/images/userlogin_mid.gif | Bin 0 -> 55 bytes ui/images/v1_logo.gif | Bin 0 -> 8453 bytes ui/images/v1_popupbuttonbg.gif | Bin 0 -> 920 bytes ui/images/v1_popupbuttonbg_hover.gif | Bin 0 -> 894 bytes ui/images/v1_popupheaderbg.gif | Bin 0 -> 592 bytes ui/images/v1_vmopoup_left.png | Bin 0 -> 1930 bytes ui/images/v1_vmopoup_mid.png | Bin 0 -> 395 bytes ui/images/v1_vmopoup_right.png | Bin 0 -> 2905 bytes ui/images/v1adddiskoff_button.gif | Bin 0 -> 1798 bytes ui/images/v1adddiskoff_button_hover.gif | Bin 0 -> 1887 bytes ui/images/v1addhost_button.gif | Bin 0 -> 1278 bytes ui/images/v1addhost_button_hover.gif | Bin 0 -> 1351 bytes ui/images/v1addiso_button.gif | Bin 0 -> 1387 bytes ui/images/v1addiso_button_hover.gif | Bin 0 -> 1442 bytes ui/images/v1addloadbalncer_button.gif | Bin 0 -> 2822 bytes ui/images/v1addloadbalncer_button_hover.gif | Bin 0 -> 2710 bytes ui/images/v1addnewvm_button.gif | Bin 0 -> 1755 bytes ui/images/v1addnewvm_button_hover.gif | Bin 0 -> 1903 bytes ui/images/v1addpstorage_button.gif | Bin 0 -> 1987 bytes ui/images/v1addpstorage_button_hover.gif | Bin 0 -> 2037 bytes ui/images/v1addserviceoff_button.gif | Bin 0 -> 1951 bytes ui/images/v1addserviceoff_button_hover.gif | Bin 0 -> 2022 bytes ui/images/v1addsggroup_button.gif | Bin 0 -> 2868 bytes ui/images/v1addsggroup_button_hover.gif | Bin 0 -> 2946 bytes ui/images/v1addsstorage_button.gif | Bin 0 -> 2126 bytes ui/images/v1addsstorage_button_hover.gif | Bin 0 -> 2239 bytes ui/images/v1addtemplate_button.gif | Bin 0 -> 1649 bytes ui/images/v1addtemplate_button_hover.gif | Bin 0 -> 1725 bytes ui/images/v1addvolume_button.gif | Bin 0 -> 1442 bytes ui/images/v1addvolume_button_hover.gif | Bin 0 -> 1488 bytes ui/images/v1admin_menutab_off.gif | Bin 0 -> 1611 bytes ui/images/v1admin_menutab_off_hover.gif | Bin 0 -> 1589 bytes ui/images/v1admin_menutab_on.gif | Bin 0 -> 250 bytes ui/images/v1aquireip_button.gif | Bin 0 -> 1611 bytes ui/images/v1aquireip_button_hover.gif | Bin 0 -> 1717 bytes ui/images/v1grid_morebutton.gif | Bin 0 -> 1754 bytes ui/images/v1grid_morebutton_hover.gif | Bin 0 -> 1313 bytes ui/images/v1gridheader_bg.gif | Bin 0 -> 357 bytes ui/images/v1header_bg.gif | Bin 0 -> 539 bytes ui/images/v1menutab_off.gif | Bin 0 -> 2226 bytes ui/images/v1menutab_off_hover.gif | Bin 0 -> 2137 bytes ui/images/v1menutab_on.gif | Bin 0 -> 381 bytes ui/images/v1stepbox_slected.gif | Bin 0 -> 791 bytes ui/images/v1stepcontainer_bg.gif | Bin 0 -> 741 bytes ui/images/vm_actionbg.gif | Bin 0 -> 230 bytes ui/images/vm_continuebutton.gif | Bin 0 -> 950 bytes ui/images/vm_continuebutton_hover.gif | Bin 0 -> 1390 bytes ui/images/vm_greenarrow.gif | Bin 0 -> 131 bytes ui/images/vm_greyarrow.gif | Bin 0 -> 122 bytes ui/images/vm_redarrow.gif | Bin 0 -> 129 bytes ui/images/vmbot_loader.gif | Bin 0 -> 1849 bytes ui/images/vmdropdown_closebutt.gif | Bin 0 -> 510 bytes ui/images/vmdropdown_closebutt_hover.gif | Bin 0 -> 520 bytes ui/images/vmopoup_left.png | Bin 0 -> 3770 bytes ui/images/vmopoup_mid.png | Bin 0 -> 691 bytes ui/images/vmopoup_right.png | Bin 0 -> 2962 bytes ui/images/windowsconsole_icon.gif | Bin 0 -> 1422 bytes ui/images/windowsconsole_iconhover.gif | Bin 0 -> 1373 bytes ui/images/wizard_contentbg.gif | Bin 0 -> 201 bytes ui/images/working_vm.gif | Bin 0 -> 3112 bytes ui/images/yellow_statusbar.gif | Bin 0 -> 529 bytes ui/images/zone_addicon.png | Bin 0 -> 880 bytes ui/images/zone_addipbutton.gif | Bin 0 -> 1574 bytes ui/images/zone_addipbutton_hover.gif | Bin 0 -> 1581 bytes ui/images/zone_addpodbutton.gif | Bin 0 -> 1124 bytes ui/images/zone_addpodbutton_hover.gif | Bin 0 -> 1137 bytes ui/images/zone_addpubIP.gif | Bin 0 -> 1547 bytes ui/images/zone_addpubIP_hover.gif | Bin 0 -> 1552 bytes ui/images/zone_changeipbutton.gif | Bin 0 -> 1212 bytes ui/images/zone_changeipbutton_hover.gif | Bin 0 -> 1649 bytes ui/images/zone_deletebutton.gif | Bin 0 -> 1017 bytes ui/images/zone_deletebutton_hover.gif | Bin 0 -> 1022 bytes ui/images/zone_detailsboxleft.gif | Bin 0 -> 2310 bytes ui/images/zone_detailsboxmid.gif | Bin 0 -> 248 bytes ui/images/zone_detailsboxright.gif | Bin 0 -> 1805 bytes ui/images/zone_directipbutton.gif | Bin 0 -> 1530 bytes ui/images/zone_directipbutton_hover.gif | Bin 0 -> 1540 bytes ui/images/zone_directipicon.png | Bin 0 -> 1024 bytes ui/images/zone_editpodbutton.gif | Bin 0 -> 1090 bytes ui/images/zone_editpodbutton_hover.gif | Bin 0 -> 1115 bytes ui/images/zone_editzonebutton.gif | Bin 0 -> 1131 bytes ui/images/zone_editzonebutton_hover.gif | Bin 0 -> 1137 bytes ui/images/zone_enablefirewallbutton.gif | Bin 0 -> 1382 bytes ui/images/zone_enablefirewallbutton_hover.gif | Bin 0 -> 1874 bytes ui/images/zone_ipicon.png | Bin 0 -> 1023 bytes ui/images/zone_openarrow.png | Bin 0 -> 251 bytes ui/images/zone_podicon.png | Bin 0 -> 1055 bytes ui/images/zone_sidearrow.png | Bin 0 -> 283 bytes ui/images/zone_zoneicon.png | Bin 0 -> 1284 bytes ui/images/zonedetails_left.gif | Bin 0 -> 6261 bytes ui/images/zonedetails_mid.gif | Bin 0 -> 476 bytes ui/images/zonedetails_right.gif | Bin 0 -> 5046 bytes ui/images/zonestitle_icons.gif | Bin 0 -> 1560 bytes ui/images/zonetree_addbutton.jpg | Bin 0 -> 2679 bytes ui/images/zonetree_blankbutton.jpg | Bin 0 -> 1678 bytes ui/images/zonetree_left.gif | Bin 0 -> 2547 bytes ui/images/zonetree_mid.gif | Bin 0 -> 387 bytes ui/images/zonetree_nonselectedbutton.jpg | Bin 0 -> 2328 bytes ui/images/zonetree_right.gif | Bin 0 -> 2864 bytes ui/images/zonetree_selectedbutton.jpg | Bin 0 -> 2505 bytes ui/index.html | 167 + ui/jsp/tab_domains.jsp | 157 + ui/resources/resource.properties | 5 + ui/resources/resource_zh.properties | 5 + ui/scripts/cloud.core.accounts.js | 322 + ui/scripts/cloud.core.callbacks.js | 91 + ui/scripts/cloud.core.configuration.js | 1753 +++ ui/scripts/cloud.core.domains.js | 399 + ui/scripts/cloud.core.events.js | 284 + ui/scripts/cloud.core.hosts.js | 714 ++ ui/scripts/cloud.core.init.js | 804 ++ ui/scripts/cloud.core.instances.js | 2646 ++++ ui/scripts/cloud.core.js | 859 ++ ui/scripts/cloud.core.network.js | 1759 +++ ui/scripts/cloud.core.storage.js | 2216 ++++ ui/scripts/cloud.core.templates.js | 1082 ++ ui/scripts/cloud.logger.js | 288 + ui/scripts/date.js | 127 + ui/scripts/jquery-1.4.2.min.js | 154 + ui/scripts/jquery-ui-1.8.2.custom.min.js | 1012 ++ ui/scripts/jquery.cookies.js | 96 + ui/scripts/jquery.md5.js | 229 + ui/scripts/jquery.timers.js | 138 + ui/test/content/tab_test.html | 484 + ui/test/index.html | 110 + ui/test/scripts/cloud.core.test.js | 885 ++ utils/.classpath | 13 + utils/.project | 17 + utils/conf/db.properties | 40 + utils/conf/log4j-vmops.xml | 76 + utils/src/com/cloud/utils/ActionDelegate.java | 23 + utils/src/com/cloud/utils/DateUtil.java | 270 + utils/src/com/cloud/utils/EnumUtils.java | 30 + .../src/com/cloud/utils/EnumerationImpl.java | 47 + utils/src/com/cloud/utils/IteratorUtil.java | 56 + utils/src/com/cloud/utils/NumbersUtil.java | 271 + utils/src/com/cloud/utils/Pair.java | 72 + .../com/cloud/utils/PasswordGenerator.java | 75 + utils/src/com/cloud/utils/ProcessUtil.java | 124 + utils/src/com/cloud/utils/Profiler.java | 43 + utils/src/com/cloud/utils/PropertiesUtil.java | 97 + utils/src/com/cloud/utils/ReflectUtil.java | 54 + .../src/com/cloud/utils/SerialVersionUID.java | 58 + utils/src/com/cloud/utils/StringUtils.java | 41 + utils/src/com/cloud/utils/Ternary.java | 83 + utils/src/com/cloud/utils/UUID.java | 282 + utils/src/com/cloud/utils/UriUtils.java | 54 + .../cloud/utils/backoff/BackoffAlgorithm.java | 36 + .../backoff/impl/ConstantTimeBackoff.java | 114 + .../impl/ConstantTimeBackoffMBean.java | 37 + .../com/cloud/utils/component/Adapter.java | 66 + .../cloud/utils/component/AdapterBase.java | 49 + .../com/cloud/utils/component/Adapters.java | 66 + .../utils/component/ComponentLocator.java | 791 ++ .../component/ComponentLocatorMBean.java | 49 + .../src/com/cloud/utils/component/Inject.java | 29 + .../com/cloud/utils/component/Manager.java | 62 + .../utils/concurrency/NamedThreadFactory.java | 36 + .../cloud/utils/concurrency/Scheduler.java | 30 + .../cloud/utils/concurrency/TestClock.java | 138 + utils/src/com/cloud/utils/db/Attribute.java | 225 + utils/src/com/cloud/utils/db/DB.java | 63 + utils/src/com/cloud/utils/db/DaoSearch.java | 21 + utils/src/com/cloud/utils/db/DataStore.java | 27 + .../com/cloud/utils/db/DatabaseCallback.java | 36 + .../utils/db/DatabaseCallbackFilter.java | 45 + utils/src/com/cloud/utils/db/DbUtil.java | 284 + utils/src/com/cloud/utils/db/Filter.java | 117 + utils/src/com/cloud/utils/db/GenericDao.java | 216 + .../com/cloud/utils/db/GenericDaoBase.java | 1362 ++ .../cloud/utils/db/GenericSearchBuilder.java | 407 + utils/src/com/cloud/utils/db/GlobalLock.java | 230 + utils/src/com/cloud/utils/db/Merovingian.java | 358 + .../src/com/cloud/utils/db/SearchBuilder.java | 70 + .../com/cloud/utils/db/SearchCriteria.java | 313 + .../com/cloud/utils/db/SequenceFetcher.java | 151 + .../src/com/cloud/utils/db/SqlGenerator.java | 597 + utils/src/com/cloud/utils/db/Transaction.java | 866 ++ .../src/com/cloud/utils/db/UpdateBuilder.java | 139 + .../src/com/cloud/utils/encoding/Base64.java | 1758 +++ .../src/com/cloud/utils/events/EventArgs.java | 43 + .../com/cloud/utils/events/EventsTest.java | 68 + .../cloud/utils/events/SubscriptionMgr.java | 170 + .../exception/CloudRuntimeException.java | 39 + .../cloud/utils/exception/ExceptionUtil.java | 33 + .../utils/exception/ExecutionException.java | 33 + .../src/com/cloud/utils/fsm/StateMachine.java | 147 + utils/src/com/cloud/utils/mgmt/JmxUtil.java | 39 + .../com/cloud/utils/mgmt/ManagementBean.java | 27 + utils/src/com/cloud/utils/net/Ip4Address.java | 38 + utils/src/com/cloud/utils/net/MacAddress.java | 390 + utils/src/com/cloud/utils/net/NetUtils.java | 758 ++ utils/src/com/cloud/utils/net/NfsUtils.java | 54 + utils/src/com/cloud/utils/net/UrlUtil.java | 56 + .../com/cloud/utils/nio/HandlerFactory.java | 26 + utils/src/com/cloud/utils/nio/Link.java | 294 + utils/src/com/cloud/utils/nio/NioClient.java | 76 + .../com/cloud/utils/nio/NioConnection.java | 391 + utils/src/com/cloud/utils/nio/NioServer.java | 85 + utils/src/com/cloud/utils/nio/Task.java | 90 + .../src/com/cloud/utils/script/Executor.java | 28 + .../cloud/utils/script/OutputInterpreter.java | 145 + utils/src/com/cloud/utils/script/Script.java | 407 + .../com/cloud/utils/script/ScriptBuilder.java | 75 + .../cloud/utils/testcase/ComponentSetup.java | 30 + .../utils/testcase/ComponentTestCase.java | 45 + .../utils/testcase/Log4jEnabledTestCase.java | 56 + .../com/cloud/utils/time/InaccurateClock.java | 54 + utils/src/javax/ejb/Local.java | 56 + .../persistence/AssociationOverride.java | 97 + .../persistence/AssociationOverrides.java | 88 + .../javax/persistence/AttributeOverride.java | 105 + .../javax/persistence/AttributeOverrides.java | 70 + utils/src/javax/persistence/Basic.java | 86 + utils/src/javax/persistence/CascadeType.java | 62 + utils/src/javax/persistence/Column.java | 140 + utils/src/javax/persistence/ColumnResult.java | 80 + .../persistence/DiscriminatorColumn.java | 101 + .../javax/persistence/DiscriminatorType.java | 53 + .../javax/persistence/DiscriminatorValue.java | 95 + utils/src/javax/persistence/Embeddable.java | 62 + utils/src/javax/persistence/Embedded.java | 67 + utils/src/javax/persistence/EmbeddedId.java | 62 + utils/src/javax/persistence/Entity.java | 59 + .../persistence/EntityExistsException.java | 87 + .../javax/persistence/EntityListeners.java | 57 + .../persistence/EntityNotFoundException.java | 71 + utils/src/javax/persistence/EntityResult.java | 86 + .../javax/persistence/EntityTransaction.java | 90 + utils/src/javax/persistence/EnumType.java | 51 + utils/src/javax/persistence/Enumerated.java | 73 + utils/src/javax/persistence/FetchType.java | 65 + utils/src/javax/persistence/FieldResult.java | 81 + .../src/javax/persistence/FlushModeType.java | 67 + .../src/javax/persistence/GeneratedValue.java | 90 + .../src/javax/persistence/GenerationType.java | 74 + utils/src/javax/persistence/Id.java | 59 + utils/src/javax/persistence/IdClass.java | 73 + utils/src/javax/persistence/Inheritance.java | 70 + .../javax/persistence/InheritanceType.java | 58 + utils/src/javax/persistence/JoinColumn.java | 139 + utils/src/javax/persistence/JoinColumns.java | 71 + utils/src/javax/persistence/JoinTable.java | 122 + utils/src/javax/persistence/Lob.java | 68 + utils/src/javax/persistence/LockModeType.java | 86 + utils/src/javax/persistence/ManyToMany.java | 146 + utils/src/javax/persistence/ManyToOne.java | 99 + utils/src/javax/persistence/MapKey.java | 120 + .../javax/persistence/MappedSuperclass.java | 152 + utils/src/javax/persistence/OneToMany.java | 123 + utils/src/javax/persistence/OneToOne.java | 134 + utils/src/javax/persistence/OrderBy.java | 110 + .../persistence/PersistenceException.java | 86 + utils/src/javax/persistence/PostLoad.java | 54 + utils/src/javax/persistence/PostPersist.java | 54 + utils/src/javax/persistence/PostRemove.java | 54 + utils/src/javax/persistence/PostUpdate.java | 54 + .../persistence/PrimaryKeyJoinColumn.java | 120 + .../persistence/PrimaryKeyJoinColumns.java | 73 + .../src/javax/persistence/SecondaryTable.java | 120 + .../javax/persistence/SecondaryTables.java | 80 + .../javax/persistence/SequenceGenerator.java | 91 + .../persistence/SqlResultSetMapping.java | 87 + .../persistence/SqlResultSetMappings.java | 54 + utils/src/javax/persistence/Table.java | 91 + .../src/javax/persistence/TableGenerator.java | 157 + utils/src/javax/persistence/Temporal.java | 69 + utils/src/javax/persistence/TemporalType.java | 54 + utils/src/javax/persistence/Transient.java | 64 + .../javax/persistence/UniqueConstraint.java | 67 + utils/src/javax/persistence/Version.java | 77 + utils/src/javax/persistence/package.html | 19 + utils/test/com/cloud/utils/TestProfiler.java | 47 + version-info.in | 10 + .../SYSCONFDIR/rc.d/init.d/cloud-vnetd.in | 85 + .../SYSCONFDIR/rc.d/init.d/cloud-vnetd.in | 85 + .../ubuntu/SYSCONFDIR/init.d/cloud-vnetd.in | 104 + vnet/sbindir/cloud-vn.in | 904 ++ vnet/src/00INSTALL | 55 + vnet/src/00README | 15 + vnet/src/Make.env | 28 + vnet/src/Makefile | 89 + vnet/src/doc/Makefile | 51 + vnet/src/doc/man/vn.pod.1 | 176 + vnet/src/doc/vnet-module.txt | 72 + vnet/src/doc/vnet-xend.txt | 167 + vnet/src/examples/Makefile | 18 + vnet/src/examples/network-vnet | 10 + vnet/src/examples/vnet-insert | 28 + vnet/src/examples/vnet97.sxp | 2 + vnet/src/examples/vnet98.sxp | 2 + vnet/src/examples/vnet99.sxp | 2 + vnet/src/libxutil/Makefile | 86 + vnet/src/libxutil/allocate.c | 116 + vnet/src/libxutil/allocate.h | 45 + vnet/src/libxutil/debug.h | 72 + vnet/src/libxutil/enum.c | 61 + vnet/src/libxutil/enum.h | 30 + vnet/src/libxutil/fd_stream.c | 184 + vnet/src/libxutil/fd_stream.h | 36 + vnet/src/libxutil/file_stream.c | 234 + vnet/src/libxutil/file_stream.h | 35 + vnet/src/libxutil/gzip_stream.c | 174 + vnet/src/libxutil/gzip_stream.h | 30 + vnet/src/libxutil/hash_table.c | 658 + vnet/src/libxutil/hash_table.h | 240 + vnet/src/libxutil/iostream.c | 55 + vnet/src/libxutil/iostream.h | 269 + vnet/src/libxutil/kernel_stream.c | 178 + vnet/src/libxutil/kernel_stream.h | 29 + vnet/src/libxutil/lexis.c | 94 + vnet/src/libxutil/lexis.h | 128 + vnet/src/libxutil/mem_stream.c | 324 + vnet/src/libxutil/mem_stream.h | 32 + vnet/src/libxutil/socket_stream.c | 230 + vnet/src/libxutil/socket_stream.h | 53 + vnet/src/libxutil/string_stream.c | 162 + vnet/src/libxutil/string_stream.h | 45 + vnet/src/libxutil/sxpr.c | 1230 ++ vnet/src/libxutil/sxpr.h | 440 + vnet/src/libxutil/sxpr_parser.c | 1002 ++ vnet/src/libxutil/sxpr_parser.h | 160 + vnet/src/libxutil/sys_net.c | 319 + vnet/src/libxutil/sys_net.h | 78 + vnet/src/libxutil/sys_string.c | 218 + vnet/src/libxutil/sys_string.h | 95 + vnet/src/libxutil/util.c | 106 + vnet/src/libxutil/util.h | 28 + vnet/src/scripts/Makefile | 15 + vnet/src/scripts/vn | 904 ++ vnet/src/vnet-module/00README | 39 + vnet/src/vnet-module/Makefile | 71 + vnet/src/vnet-module/Makefile-2.4 | 98 + vnet/src/vnet-module/Makefile-2.6 | 60 + vnet/src/vnet-module/Makefile.ver | 51 + vnet/src/vnet-module/Makefile.vnet | 62 + vnet/src/vnet-module/esp.c | 903 ++ vnet/src/vnet-module/esp.h | 120 + vnet/src/vnet-module/etherip.c | 488 + vnet/src/vnet-module/etherip.h | 38 + vnet/src/vnet-module/if_etherip.h | 83 + vnet/src/vnet-module/if_varp.h | 105 + vnet/src/vnet-module/linux/pfkeyv2.h | 329 + vnet/src/vnet-module/random.c | 87 + vnet/src/vnet-module/random.h | 28 + vnet/src/vnet-module/sa.c | 756 ++ vnet/src/vnet-module/sa.h | 215 + vnet/src/vnet-module/sa_algorithm.c | 367 + vnet/src/vnet-module/sa_algorithm.h | 63 + vnet/src/vnet-module/skb_context.c | 92 + vnet/src/vnet-module/skb_context.h | 87 + vnet/src/vnet-module/skb_util.c | 579 + vnet/src/vnet-module/skb_util.h | 154 + vnet/src/vnet-module/sxpr_util.c | 119 + vnet/src/vnet-module/sxpr_util.h | 36 + vnet/src/vnet-module/timer_util.c | 76 + vnet/src/vnet-module/timer_util.h | 41 + vnet/src/vnet-module/tunnel.c | 270 + vnet/src/vnet-module/tunnel.h | 113 + vnet/src/vnet-module/varp.c | 1559 +++ vnet/src/vnet-module/varp.h | 161 + vnet/src/vnet-module/varp_socket.c | 901 ++ vnet/src/vnet-module/varp_util.c | 77 + vnet/src/vnet-module/varp_util.h | 95 + vnet/src/vnet-module/vif.c | 389 + vnet/src/vnet-module/vif.h | 64 + vnet/src/vnet-module/vnet.c | 712 ++ vnet/src/vnet-module/vnet.h | 108 + vnet/src/vnet-module/vnet_dev.c | 296 + vnet/src/vnet-module/vnet_dev.h | 27 + vnet/src/vnet-module/vnet_eval.c | 378 + vnet/src/vnet-module/vnet_eval.h | 35 + vnet/src/vnet-module/vnet_forward.c | 389 + vnet/src/vnet-module/vnet_forward.h | 36 + vnet/src/vnet-module/vnet_ioctl.c | 509 + vnet/src/vnet-module/vnet_ioctl.h | 25 + vnet/src/vnetd/Makefile | 121 + vnet/src/vnetd/connection.c | 412 + vnet/src/vnetd/connection.h | 76 + vnet/src/vnetd/list.h | 284 + vnet/src/vnetd/select.c | 109 + vnet/src/vnetd/select.h | 60 + vnet/src/vnetd/selector.c | 133 + vnet/src/vnetd/selector.h | 62 + vnet/src/vnetd/skbuff.c | 530 + vnet/src/vnetd/skbuff.h | 538 + vnet/src/vnetd/spinlock.c | 65 + vnet/src/vnetd/spinlock.h | 47 + vnet/src/vnetd/sys_kernel.h | 71 + vnet/src/vnetd/timer.c | 157 + vnet/src/vnetd/timer.h | 40 + vnet/src/vnetd/vnetd.c | 1188 ++ waf | Bin 0 -> 92487 bytes waf.bat | 28 + wscript | 815 ++ wscript_build | 574 + wscript_configure | 285 + 2098 files changed, 297952 insertions(+) create mode 100644 HACKING create mode 100644 INSTALL create mode 100644 README create mode 100644 README.html create mode 100644 agent/.classpath create mode 100644 agent/.project create mode 100755 agent/bindir/cloud-setup-agent.in create mode 100644 agent/conf/agent.properties create mode 100644 agent/conf/developer.properties.template create mode 100644 agent/conf/environment.properties.in create mode 100644 agent/conf/log4j-cloud.xml.in create mode 100755 agent/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-agent.in create mode 100755 agent/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-agent.in create mode 100755 agent/distro/ubuntu/SYSCONFDIR/init.d/cloud-agent.in create mode 100644 agent/distro/ubuntu/SYSCONFDIR/init/cloud-cleanup.conf create mode 100644 agent/doc/README-iscsi.txt create mode 100644 agent/doc/README.txt create mode 100755 agent/libexec/agent-runner.in create mode 100644 agent/patch/patch.tgz create mode 100755 agent/patch/redopatch.sh create mode 100755 agent/scripts/agent.sh create mode 100755 agent/scripts/run.sh create mode 100755 agent/src/com/cloud/agent/Agent.java create mode 100644 agent/src/com/cloud/agent/AgentShell.java create mode 100644 agent/src/com/cloud/agent/IAgentShell.java create mode 100755 agent/src/com/cloud/agent/dao/StorageComponent.java create mode 100755 agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java create mode 100755 agent/src/com/cloud/agent/resource/DummyResource.java create mode 100644 agent/src/com/cloud/agent/resource/computing/LibvirtCapXMLParser.java create mode 100644 agent/src/com/cloud/agent/resource/computing/LibvirtComputingResource.java create mode 100644 agent/src/com/cloud/agent/resource/computing/LibvirtDomainXMLParser.java create mode 100644 agent/src/com/cloud/agent/resource/computing/LibvirtNetworkDef.java create mode 100644 agent/src/com/cloud/agent/resource/computing/LibvirtStoragePoolDef.java create mode 100644 agent/src/com/cloud/agent/resource/computing/LibvirtStorageVolumeDef.java create mode 100644 agent/src/com/cloud/agent/resource/computing/LibvirtVMDef.java create mode 100644 agent/src/com/cloud/agent/resource/computing/LibvirtXMLParser.java create mode 100644 agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java create mode 100644 agent/src/com/cloud/agent/resource/storage/IscsiMountPreparer.java create mode 100644 agent/test/com/cloud/agent/TestAgentShell.java create mode 100644 api/.classpath create mode 100644 api/.project create mode 100644 api/src/com/cloud/deploy/DataCenterDeployment.java create mode 100644 api/src/com/cloud/deploy/DeploymentStrategy.java create mode 100644 api/src/com/cloud/exception/AgentUnavailableException.java create mode 100644 api/src/com/cloud/exception/ConcurrentOperationException.java create mode 100644 api/src/com/cloud/exception/DiscoveryException.java create mode 100644 api/src/com/cloud/exception/HAStateException.java create mode 100644 api/src/com/cloud/exception/InsufficientAddressCapacityException.java create mode 100755 api/src/com/cloud/exception/InsufficientCapacityException.java create mode 100755 api/src/com/cloud/exception/InsufficientStorageCapacityException.java create mode 100644 api/src/com/cloud/exception/InsufficientVirtualNetworkCapcityException.java create mode 100644 api/src/com/cloud/exception/InternalErrorException.java create mode 100644 api/src/com/cloud/exception/InvalidParameterValueException.java create mode 100644 api/src/com/cloud/exception/ManagementServerException.java create mode 100644 api/src/com/cloud/exception/NetworkRuleConflictException.java create mode 100644 api/src/com/cloud/exception/PermissionDeniedException.java create mode 100644 api/src/com/cloud/exception/ResourceAllocationException.java create mode 100644 api/src/com/cloud/exception/ResourceInUseException.java create mode 100644 api/src/com/cloud/exception/StorageUnavailableException.java create mode 100644 api/src/com/cloud/hypervisor/Hypervisor.java create mode 100644 api/src/com/cloud/network/Network.java create mode 100644 api/src/com/cloud/offering/DiskOffering.java create mode 100644 api/src/com/cloud/offering/NetworkOffering.java create mode 100644 api/src/com/cloud/offering/OfferingManager.java create mode 100755 api/src/com/cloud/offering/ServiceOffering.java create mode 100644 api/src/com/cloud/storage/Storage.java create mode 100755 api/src/com/cloud/uservm/UserVm.java create mode 100644 api/src/com/cloud/vm/Nic.java create mode 100644 api/src/com/cloud/vm/State.java create mode 100755 api/src/com/cloud/vm/VirtualMachine.java create mode 100644 api/src/com/cloud/vm/VmCharacteristics.java create mode 100755 build.xml create mode 100755 build/build-cloud.properties create mode 100755 build/build-cloud.xml create mode 100755 build/build-common.xml create mode 100755 build/build-docs.xml create mode 100644 build/build.number create mode 100755 build/cloud.properties create mode 100644 build/deploy/branding/default/images/favicon.ico create mode 100644 build/deploy/branding/default/images/header_logo.gif create mode 100644 build/deploy/branding/godaddy/images/header_logo.gif create mode 100644 build/deploy/branding/nframe/images/header_logo.gif create mode 100644 build/deploy/branding/superb/images/header_logo.gif create mode 100755 build/deploy/db/deploy-db.sh create mode 100644 build/deploy/db/log4j.properties create mode 100755 build/deploy/deploy-agent.sh create mode 100644 build/deploy/deploy-console-proxy.sh create mode 100755 build/deploy/deploy-server.sh create mode 100644 build/deploy/deploy-simulator.sh create mode 100755 build/deploy/install-storage-server.sh create mode 100644 build/deploy/install.sh create mode 100644 build/deploy/production/agent/storagehdpatch/etc/default/init create mode 100644 build/deploy/production/agent/storagehdpatch/etc/inet/ntp.conf create mode 100644 build/deploy/production/agent/storagehdpatch/etc/nsswitch.conf create mode 100644 build/deploy/production/agent/storagehdpatch/etc/ssh/sshd_config create mode 100644 build/deploy/production/agent/storagehdpatch/etc/system create mode 100644 build/deploy/production/agent/storagehdpatch/etc/vmops/disks.properties create mode 100644 build/deploy/production/agent/storagehdpatch/etc/vmops/network.properties create mode 100755 build/deploy/production/agent/storagehdpatch/lib/svc/method/vmops create mode 100755 build/deploy/production/agent/storagehdpatch/usr/sbin/vsetup create mode 100644 build/deploy/production/agent/storagehdpatch/var/svc/manifest/application/cloud.xml create mode 100644 build/deploy/production/consoleproxy/conf/consoleproxy.properties create mode 100644 build/deploy/production/db/server-setup-dev.xml create mode 100644 build/deploy/production/db/templates-dev.sql create mode 100644 build/deploy/production/premium/conf/log4j-cloud_usage.xml create mode 100644 build/deploy/production/premium/conf/log4j-cloud_usage.xml.template create mode 100644 build/deploy/production/premium/conf/usage-components.xml create mode 100644 build/deploy/production/server/conf/agent-update.properties create mode 100644 build/deploy/production/server/conf/cloud-localhost.pk12 create mode 100755 build/deploy/production/server/conf/ehcache.xml create mode 100755 build/deploy/production/server/conf/log4j-cloud.xml create mode 100644 build/deploy/production/server/conf/log4j-cloud.xml.template create mode 100755 build/deploy/production/server/conf/server.xml create mode 100755 build/developer.xml create mode 100755 build/overview.html create mode 100755 build/package.xml create mode 100644 build/release-notes create mode 100644 build/replace.properties create mode 100644 client/.classpath create mode 100644 client/.project create mode 100644 client/WEB-INF/web.xml create mode 100755 client/bindir/cloud-setup-management.in create mode 100755 client/bindir/cloud-update-xenserver-licenses.in create mode 100755 client/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-management.in create mode 100644 client/distro/centos/SYSCONFDIR/sysconfig/cloud-management.in create mode 100755 client/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-management.in create mode 100644 client/distro/fedora/SYSCONFDIR/sysconfig/cloud-management.in create mode 100755 client/distro/ubuntu/SYSCONFDIR/init.d/cloud-management.in create mode 100644 client/tomcatconf/catalina.policy.in create mode 100644 client/tomcatconf/catalina.properties.in create mode 100644 client/tomcatconf/classpath.conf.in create mode 100644 client/tomcatconf/commands.properties.in create mode 100755 client/tomcatconf/components.xml.in create mode 100644 client/tomcatconf/context.xml.in create mode 100644 client/tomcatconf/db.properties.in create mode 100755 client/tomcatconf/ehcache.xml.in create mode 100644 client/tomcatconf/environment.properties.in create mode 100755 client/tomcatconf/log4j-cloud.xml.in create mode 100644 client/tomcatconf/logging.properties.in create mode 100755 client/tomcatconf/server.xml.in create mode 100644 client/tomcatconf/tomcat-users.xml.in create mode 100644 client/tomcatconf/tomcat6.conf.in create mode 100644 client/tomcatconf/web.xml.in create mode 100644 cloud.spec create mode 100644 configure-info.in create mode 100644 console-proxy/.classpath create mode 100644 console-proxy/.project create mode 100755 console-proxy/bindir/cloud-setup-console-proxy.in create mode 100644 console-proxy/conf.dom0/agent.properties.in create mode 100644 console-proxy/conf.dom0/consoleproxy.properties.in create mode 100644 console-proxy/conf.dom0/log4j-cloud.xml.in create mode 100644 console-proxy/conf/agent.properties create mode 100644 console-proxy/conf/consoleproxy.properties create mode 100644 console-proxy/conf/log4j-cloud.xml create mode 100644 console-proxy/css/ajaxviewer.css create mode 100644 console-proxy/css/logger.css create mode 100644 console-proxy/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-console-proxy.in create mode 100644 console-proxy/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-console-proxy.in create mode 100755 console-proxy/distro/ubuntu/SYSCONFDIR/init.d/cloud-console-proxy.in create mode 100644 console-proxy/images/back.gif create mode 100644 console-proxy/images/bright-green.png create mode 100644 console-proxy/images/cad.gif create mode 100644 console-proxy/images/cannotconnect.jpg create mode 100644 console-proxy/images/clr_button.gif create mode 100644 console-proxy/images/clr_button_hover.gif create mode 100644 console-proxy/images/dot.cur create mode 100644 console-proxy/images/gray-green.png create mode 100644 console-proxy/images/grid_headerbg.gif create mode 100644 console-proxy/images/left.png create mode 100644 console-proxy/images/minimize_button.gif create mode 100644 console-proxy/images/minimize_button_hover.gif create mode 100644 console-proxy/images/notready.jpg create mode 100644 console-proxy/images/play_button.gif create mode 100644 console-proxy/images/play_button_hover.gif create mode 100644 console-proxy/images/right.png create mode 100644 console-proxy/images/shrink_button.gif create mode 100644 console-proxy/images/shrink_button_hover.gif create mode 100644 console-proxy/images/stop_button.gif create mode 100644 console-proxy/images/stop_button_hover.gif create mode 100644 console-proxy/images/winlog.png create mode 100644 console-proxy/js/ajaxviewer.js create mode 100644 console-proxy/js/cloud.logger.js create mode 100644 console-proxy/js/handler.js create mode 100644 console-proxy/js/jquery.js create mode 100755 console-proxy/libexec/console-proxy-runner.in create mode 100755 console-proxy/scripts/config_auth.sh create mode 100755 console-proxy/scripts/config_ssl.sh create mode 100644 console-proxy/scripts/run-proxy.sh create mode 100644 console-proxy/scripts/run.bat create mode 100755 console-proxy/scripts/run.sh create mode 100644 console-proxy/scripts/ssvm-check.sh create mode 100644 console-proxy/src/com/cloud/consoleproxy/AjaxFIFOImageCache.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxKeyMapper.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyBaseServerFactoryImpl.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientHandler.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyCmdHandler.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyJarHandler.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyLoggerFactory.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyResourceHandler.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyServerFactory.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyStatus.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyThumbnailHandler.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ConsoleProxyViewer.java create mode 100644 console-proxy/src/com/cloud/consoleproxy/ViewerOptions.java create mode 100644 console-proxy/ui/viewer-bad-sid.ftl create mode 100644 console-proxy/ui/viewer-connect-failed.ftl create mode 100644 console-proxy/ui/viewer-update.ftl create mode 100644 console-proxy/ui/viewer.ftl create mode 100644 console-proxy/vm-script/vmops create mode 100644 console-viewer/.classpath create mode 100644 console-viewer/.project create mode 100644 console-viewer/scripts/run-viewer.bat create mode 100644 console-viewer/src/com/cloud/consoleviewer/AuthPanel.java create mode 100644 console-viewer/src/com/cloud/consoleviewer/ButtonPanel.java create mode 100644 console-viewer/src/com/cloud/consoleviewer/ClipboardFrame.java create mode 100644 console-viewer/src/com/cloud/consoleviewer/ConsoleApplet.java create mode 100644 console-viewer/src/com/cloud/consoleviewer/ConsoleViewer.java create mode 100644 console-viewer/src/com/cloud/consoleviewer/OptionsFrame.java create mode 100644 console-viewer/src/com/cloud/consoleviewer/RecordingFrame.java create mode 100644 console-viewer/src/com/cloud/consoleviewer/ReloginPanel.java create mode 100644 console-viewer/test/test-applet.html create mode 100644 console/.classpath create mode 100644 console/.project create mode 100644 console/src/com/cloud/console/AuthenticationException.java create mode 100644 console/src/com/cloud/console/CapabilityInfo.java create mode 100644 console/src/com/cloud/console/CapsContainer.java create mode 100644 console/src/com/cloud/console/ConsoleCanvas.java create mode 100644 console/src/com/cloud/console/ConsoleCanvas2.java create mode 100644 console/src/com/cloud/console/DesCipher.java create mode 100644 console/src/com/cloud/console/ITileScanListener.java create mode 100644 console/src/com/cloud/console/InStream.java create mode 100644 console/src/com/cloud/console/Logger.java create mode 100644 console/src/com/cloud/console/LoggerFactory.java create mode 100644 console/src/com/cloud/console/MemInStream.java create mode 100644 console/src/com/cloud/console/Region.java create mode 100644 console/src/com/cloud/console/RegionClassifier.java create mode 100644 console/src/com/cloud/console/RfbProto.java create mode 100644 console/src/com/cloud/console/RfbProtoAdapter.java create mode 100644 console/src/com/cloud/console/RfbViewer.java create mode 100644 console/src/com/cloud/console/SessionRecorder.java create mode 100644 console/src/com/cloud/console/SplitInputStream.java create mode 100644 console/src/com/cloud/console/TileInfo.java create mode 100644 console/src/com/cloud/console/TileTracker.java create mode 100644 console/src/com/cloud/console/ZlibInStream.java create mode 100644 core/.classpath create mode 100644 core/.project create mode 100755 core/src/com/cloud/agent/AgentManager.java create mode 100644 core/src/com/cloud/agent/IAgentControl.java create mode 100644 core/src/com/cloud/agent/IAgentControlListener.java create mode 100755 core/src/com/cloud/agent/Listener.java create mode 100644 core/src/com/cloud/agent/RecoveryHandler.java create mode 100644 core/src/com/cloud/agent/api/AbstractStartCommand.java create mode 100644 core/src/com/cloud/agent/api/AgentControlAnswer.java create mode 100644 core/src/com/cloud/agent/api/AgentControlCommand.java create mode 100755 core/src/com/cloud/agent/api/Answer.java create mode 100644 core/src/com/cloud/agent/api/AttachIsoCommand.java create mode 100644 core/src/com/cloud/agent/api/AttachVolumeAnswer.java create mode 100644 core/src/com/cloud/agent/api/AttachVolumeCommand.java create mode 100644 core/src/com/cloud/agent/api/BackupSnapshotAnswer.java create mode 100644 core/src/com/cloud/agent/api/BackupSnapshotCommand.java create mode 100644 core/src/com/cloud/agent/api/CancelCommand.java create mode 100644 core/src/com/cloud/agent/api/ChangeAgentAnswer.java create mode 100644 core/src/com/cloud/agent/api/ChangeAgentCommand.java create mode 100644 core/src/com/cloud/agent/api/CheckHealthAnswer.java create mode 100644 core/src/com/cloud/agent/api/CheckHealthCommand.java create mode 100644 core/src/com/cloud/agent/api/CheckOnHostAnswer.java create mode 100644 core/src/com/cloud/agent/api/CheckOnHostCommand.java create mode 100644 core/src/com/cloud/agent/api/CheckOnVmAnswer.java create mode 100644 core/src/com/cloud/agent/api/CheckOnVmCommand.java create mode 100755 core/src/com/cloud/agent/api/CheckStateAnswer.java create mode 100755 core/src/com/cloud/agent/api/CheckStateCommand.java create mode 100644 core/src/com/cloud/agent/api/CheckVirtualMachineAnswer.java create mode 100644 core/src/com/cloud/agent/api/CheckVirtualMachineCommand.java create mode 100755 core/src/com/cloud/agent/api/Command.java create mode 100644 core/src/com/cloud/agent/api/ConsoleAccessAuthenticationAnswer.java create mode 100644 core/src/com/cloud/agent/api/ConsoleAccessAuthenticationCommand.java create mode 100644 core/src/com/cloud/agent/api/ConsoleProxyLoadReportCommand.java create mode 100644 core/src/com/cloud/agent/api/CreatePrivateTemplateFromSnapshotCommand.java create mode 100644 core/src/com/cloud/agent/api/CreateVolumeFromSnapshotAnswer.java create mode 100644 core/src/com/cloud/agent/api/CreateVolumeFromSnapshotCommand.java create mode 100755 core/src/com/cloud/agent/api/CreateZoneVlanAnswer.java create mode 100755 core/src/com/cloud/agent/api/CreateZoneVlanCommand.java create mode 100755 core/src/com/cloud/agent/api/CronCommand.java create mode 100644 core/src/com/cloud/agent/api/DeleteSnapshotBackupAnswer.java create mode 100644 core/src/com/cloud/agent/api/DeleteSnapshotBackupCommand.java create mode 100644 core/src/com/cloud/agent/api/DeleteSnapshotsDirCommand.java create mode 100644 core/src/com/cloud/agent/api/DeleteStoragePoolCommand.java create mode 100644 core/src/com/cloud/agent/api/FenceAnswer.java create mode 100644 core/src/com/cloud/agent/api/FenceCommand.java create mode 100755 core/src/com/cloud/agent/api/GetFileStatsAnswer.java create mode 100755 core/src/com/cloud/agent/api/GetFileStatsCommand.java create mode 100755 core/src/com/cloud/agent/api/GetHostStatsAnswer.java create mode 100755 core/src/com/cloud/agent/api/GetHostStatsCommand.java create mode 100755 core/src/com/cloud/agent/api/GetStorageStatsAnswer.java create mode 100755 core/src/com/cloud/agent/api/GetStorageStatsCommand.java create mode 100755 core/src/com/cloud/agent/api/GetVmStatsAnswer.java create mode 100755 core/src/com/cloud/agent/api/GetVmStatsCommand.java create mode 100644 core/src/com/cloud/agent/api/GetVncPortAnswer.java create mode 100644 core/src/com/cloud/agent/api/GetVncPortCommand.java create mode 100644 core/src/com/cloud/agent/api/HostStatsEntry.java create mode 100644 core/src/com/cloud/agent/api/MaintainAnswer.java create mode 100644 core/src/com/cloud/agent/api/MaintainCommand.java create mode 100644 core/src/com/cloud/agent/api/ManageSnapshotAnswer.java create mode 100644 core/src/com/cloud/agent/api/ManageSnapshotCommand.java create mode 100644 core/src/com/cloud/agent/api/MigrateAnswer.java create mode 100644 core/src/com/cloud/agent/api/MigrateCommand.java create mode 100644 core/src/com/cloud/agent/api/MirrorAnswer.java create mode 100644 core/src/com/cloud/agent/api/MirrorCommand.java create mode 100644 core/src/com/cloud/agent/api/ModifySshKeysCommand.java create mode 100644 core/src/com/cloud/agent/api/ModifyStoragePoolAnswer.java create mode 100644 core/src/com/cloud/agent/api/ModifyStoragePoolCommand.java create mode 100644 core/src/com/cloud/agent/api/NetworkIngressRuleAnswer.java create mode 100644 core/src/com/cloud/agent/api/NetworkIngressRulesCmd.java create mode 100644 core/src/com/cloud/agent/api/NetworkUsageAnswer.java create mode 100644 core/src/com/cloud/agent/api/NetworkUsageCommand.java create mode 100644 core/src/com/cloud/agent/api/PingAnswer.java create mode 100755 core/src/com/cloud/agent/api/PingCommand.java create mode 100755 core/src/com/cloud/agent/api/PingRoutingCommand.java create mode 100644 core/src/com/cloud/agent/api/PingRoutingWithNwGroupsCommand.java create mode 100755 core/src/com/cloud/agent/api/PingStorageCommand.java create mode 100644 core/src/com/cloud/agent/api/PingTestCommand.java create mode 100644 core/src/com/cloud/agent/api/PoolEjectCommand.java create mode 100644 core/src/com/cloud/agent/api/PrepareForMigrationAnswer.java create mode 100644 core/src/com/cloud/agent/api/PrepareForMigrationCommand.java create mode 100644 core/src/com/cloud/agent/api/ReadyAnswer.java create mode 100644 core/src/com/cloud/agent/api/ReadyCommand.java create mode 100644 core/src/com/cloud/agent/api/RebootAnswer.java create mode 100755 core/src/com/cloud/agent/api/RebootCommand.java create mode 100644 core/src/com/cloud/agent/api/RebootRouterCommand.java create mode 100644 core/src/com/cloud/agent/api/SecStorageFirewallCfgCommand.java create mode 100644 core/src/com/cloud/agent/api/SecStorageSetupCommand.java create mode 100644 core/src/com/cloud/agent/api/SetupAnswer.java create mode 100644 core/src/com/cloud/agent/api/SetupCommand.java create mode 100644 core/src/com/cloud/agent/api/ShutdownCommand.java create mode 100644 core/src/com/cloud/agent/api/SnapshotCommand.java create mode 100755 core/src/com/cloud/agent/api/StartAnswer.java create mode 100755 core/src/com/cloud/agent/api/StartCommand.java create mode 100644 core/src/com/cloud/agent/api/StartConsoleProxyAnswer.java create mode 100644 core/src/com/cloud/agent/api/StartConsoleProxyCommand.java create mode 100644 core/src/com/cloud/agent/api/StartRouterAnswer.java create mode 100755 core/src/com/cloud/agent/api/StartRouterCommand.java create mode 100644 core/src/com/cloud/agent/api/StartSecStorageVmAnswer.java create mode 100644 core/src/com/cloud/agent/api/StartSecStorageVmCommand.java create mode 100755 core/src/com/cloud/agent/api/StartupAnswer.java create mode 100755 core/src/com/cloud/agent/api/StartupCommand.java create mode 100644 core/src/com/cloud/agent/api/StartupProxyCommand.java create mode 100755 core/src/com/cloud/agent/api/StartupRoutingCommand.java create mode 100755 core/src/com/cloud/agent/api/StartupStorageCommand.java create mode 100755 core/src/com/cloud/agent/api/StopAnswer.java create mode 100755 core/src/com/cloud/agent/api/StopCommand.java create mode 100644 core/src/com/cloud/agent/api/StoragePoolInfo.java create mode 100644 core/src/com/cloud/agent/api/UnsupportedAnswer.java create mode 100644 core/src/com/cloud/agent/api/UpgradeAnswer.java create mode 100644 core/src/com/cloud/agent/api/UpgradeCommand.java create mode 100644 core/src/com/cloud/agent/api/ValidateSnapshotAnswer.java create mode 100644 core/src/com/cloud/agent/api/ValidateSnapshotCommand.java create mode 100755 core/src/com/cloud/agent/api/VmStatsEntry.java create mode 100644 core/src/com/cloud/agent/api/proxy/CheckConsoleProxyLoadCommand.java create mode 100644 core/src/com/cloud/agent/api/proxy/ConsoleProxyLoadAnswer.java create mode 100644 core/src/com/cloud/agent/api/proxy/ProxyCommand.java create mode 100644 core/src/com/cloud/agent/api/proxy/WatchConsoleProxyLoadCommand.java create mode 100644 core/src/com/cloud/agent/api/routing/DhcpEntryCommand.java create mode 100644 core/src/com/cloud/agent/api/routing/IPAssocCommand.java create mode 100644 core/src/com/cloud/agent/api/routing/LoadBalancerCfgCommand.java create mode 100644 core/src/com/cloud/agent/api/routing/RoutingCommand.java create mode 100644 core/src/com/cloud/agent/api/routing/SavePasswordCommand.java create mode 100755 core/src/com/cloud/agent/api/routing/SetFirewallRuleCommand.java create mode 100644 core/src/com/cloud/agent/api/routing/UserDataCommand.java create mode 100644 core/src/com/cloud/agent/api/routing/VmDataCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/AbstractDownloadCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/CopyVolumeAnswer.java create mode 100644 core/src/com/cloud/agent/api/storage/CopyVolumeCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/CreateAnswer.java create mode 100644 core/src/com/cloud/agent/api/storage/CreateCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/CreatePrivateTemplateAnswer.java create mode 100644 core/src/com/cloud/agent/api/storage/CreatePrivateTemplateCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/DeleteTemplateCommand.java create mode 100755 core/src/com/cloud/agent/api/storage/DestroyCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/DownloadAnswer.java create mode 100644 core/src/com/cloud/agent/api/storage/DownloadCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/DownloadProgressCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/ManageVolumeAvailabilityAnswer.java create mode 100644 core/src/com/cloud/agent/api/storage/ManageVolumeAvailabilityCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/PrimaryStorageDownloadCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/ShareAnswer.java create mode 100644 core/src/com/cloud/agent/api/storage/ShareCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/StorageCommand.java create mode 100644 core/src/com/cloud/agent/api/storage/UpgradeDiskAnswer.java create mode 100644 core/src/com/cloud/agent/api/storage/UpgradeDiskCommand.java create mode 100644 core/src/com/cloud/agent/api/to/DiskCharacteristicsTO.java create mode 100644 core/src/com/cloud/agent/api/to/HostTO.java create mode 100644 core/src/com/cloud/agent/api/to/NetworkTO.java create mode 100644 core/src/com/cloud/agent/api/to/StoragePoolTO.java create mode 100644 core/src/com/cloud/agent/api/to/TemplateTO.java create mode 100644 core/src/com/cloud/agent/api/to/VmTO.java create mode 100644 core/src/com/cloud/agent/api/to/VolumeTO.java create mode 100755 core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java create mode 100755 core/src/com/cloud/agent/transport/ArrayTypeAdaptor.java create mode 100755 core/src/com/cloud/agent/transport/Request.java create mode 100755 core/src/com/cloud/agent/transport/Response.java create mode 100644 core/src/com/cloud/agent/transport/UpgradeResponse.java create mode 100644 core/src/com/cloud/agent/transport/VolListTypeAdaptor.java create mode 100644 core/src/com/cloud/alert/AlertAdapter.java create mode 100644 core/src/com/cloud/alert/AlertManager.java create mode 100644 core/src/com/cloud/alert/AlertVO.java create mode 100644 core/src/com/cloud/alert/dao/AlertDao.java create mode 100644 core/src/com/cloud/alert/dao/AlertDaoImpl.java create mode 100644 core/src/com/cloud/async/AsyncInstanceCreateStatus.java create mode 100644 core/src/com/cloud/async/AsyncJobResult.java create mode 100644 core/src/com/cloud/async/AsyncJobVO.java create mode 100644 core/src/com/cloud/async/SyncQueueItemVO.java create mode 100644 core/src/com/cloud/async/SyncQueueVO.java create mode 100644 core/src/com/cloud/async/dao/AsyncJobDao.java create mode 100644 core/src/com/cloud/async/dao/AsyncJobDaoImpl.java create mode 100644 core/src/com/cloud/async/dao/SyncQueueDao.java create mode 100644 core/src/com/cloud/async/dao/SyncQueueDaoImpl.java create mode 100644 core/src/com/cloud/async/dao/SyncQueueItemDao.java create mode 100644 core/src/com/cloud/async/dao/SyncQueueItemDaoImpl.java create mode 100644 core/src/com/cloud/capacity/CapacityVO.java create mode 100644 core/src/com/cloud/capacity/dao/CapacityDao.java create mode 100644 core/src/com/cloud/capacity/dao/CapacityDaoImpl.java create mode 100644 core/src/com/cloud/cluster/ClusterManager.java create mode 100644 core/src/com/cloud/cluster/ClusterManagerListener.java create mode 100644 core/src/com/cloud/cluster/ClusterNodeJoinEventArgs.java create mode 100644 core/src/com/cloud/cluster/ClusterNodeLeftEventArgs.java create mode 100644 core/src/com/cloud/cluster/ClusterService.java create mode 100644 core/src/com/cloud/cluster/ManagementServerHostVO.java create mode 100644 core/src/com/cloud/cluster/RemoteMethodConstants.java create mode 100644 core/src/com/cloud/cluster/dao/ManagementServerHostDao.java create mode 100644 core/src/com/cloud/cluster/dao/ManagementServerHostDaoImpl.java create mode 100644 core/src/com/cloud/configuration/ConfigurationVO.java create mode 100644 core/src/com/cloud/configuration/ResourceCount.java create mode 100644 core/src/com/cloud/configuration/ResourceCountVO.java create mode 100644 core/src/com/cloud/configuration/ResourceLimit.java create mode 100644 core/src/com/cloud/configuration/ResourceLimitVO.java create mode 100644 core/src/com/cloud/configuration/dao/ConfigurationDao.java create mode 100644 core/src/com/cloud/configuration/dao/ConfigurationDaoImpl.java create mode 100644 core/src/com/cloud/configuration/dao/ResourceCountDao.java create mode 100644 core/src/com/cloud/configuration/dao/ResourceCountDaoImpl.java create mode 100644 core/src/com/cloud/configuration/dao/ResourceLimitDao.java create mode 100644 core/src/com/cloud/configuration/dao/ResourceLimitDaoImpl.java create mode 100644 core/src/com/cloud/consoleproxy/ConsoleProxyAlertEventArgs.java create mode 100644 core/src/com/cloud/consoleproxy/ConsoleProxyAllocator.java create mode 100644 core/src/com/cloud/consoleproxy/ConsoleProxyListener.java create mode 100644 core/src/com/cloud/consoleproxy/ConsoleProxyManager.java create mode 100644 core/src/com/cloud/dc/AccountVlanMapVO.java create mode 100644 core/src/com/cloud/dc/ClusterVO.java create mode 100755 core/src/com/cloud/dc/DataCenterIpAddressVO.java create mode 100644 core/src/com/cloud/dc/DataCenterLinkLocalIpAddressVO.java create mode 100644 core/src/com/cloud/dc/DataCenterVO.java create mode 100755 core/src/com/cloud/dc/DataCenterVnetVO.java create mode 100644 core/src/com/cloud/dc/HostPodVO.java create mode 100644 core/src/com/cloud/dc/PodCluster.java create mode 100644 core/src/com/cloud/dc/PodVlanMapVO.java create mode 100755 core/src/com/cloud/dc/PodVlanVO.java create mode 100644 core/src/com/cloud/dc/Vlan.java create mode 100644 core/src/com/cloud/dc/VlanVO.java create mode 100644 core/src/com/cloud/dc/dao/AccountVlanMapDao.java create mode 100644 core/src/com/cloud/dc/dao/AccountVlanMapDaoImpl.java create mode 100644 core/src/com/cloud/dc/dao/ClusterDao.java create mode 100644 core/src/com/cloud/dc/dao/ClusterDaoImpl.java create mode 100644 core/src/com/cloud/dc/dao/DataCenterDao.java create mode 100644 core/src/com/cloud/dc/dao/DataCenterDaoImpl.java create mode 100755 core/src/com/cloud/dc/dao/DataCenterIpAddressDaoImpl.java create mode 100644 core/src/com/cloud/dc/dao/DataCenterLinkLocalIpAddressDaoImpl.java create mode 100755 core/src/com/cloud/dc/dao/DataCenterVnetDaoImpl.java create mode 100644 core/src/com/cloud/dc/dao/HostPodDao.java create mode 100644 core/src/com/cloud/dc/dao/HostPodDaoImpl.java create mode 100755 core/src/com/cloud/dc/dao/PodVlanDaoImpl.java create mode 100644 core/src/com/cloud/dc/dao/PodVlanMapDao.java create mode 100644 core/src/com/cloud/dc/dao/PodVlanMapDaoImpl.java create mode 100644 core/src/com/cloud/dc/dao/VlanDao.java create mode 100644 core/src/com/cloud/dc/dao/VlanDaoImpl.java create mode 100644 core/src/com/cloud/domain/DomainVO.java create mode 100644 core/src/com/cloud/domain/dao/DomainDao.java create mode 100644 core/src/com/cloud/domain/dao/DomainDaoImpl.java create mode 100644 core/src/com/cloud/event/EventState.java create mode 100644 core/src/com/cloud/event/EventTypes.java create mode 100644 core/src/com/cloud/event/EventVO.java create mode 100644 core/src/com/cloud/event/dao/EventDao.java create mode 100644 core/src/com/cloud/event/dao/EventDaoImpl.java create mode 100644 core/src/com/cloud/exception/AgentControlChannelException.java create mode 100644 core/src/com/cloud/exception/OperationTimedoutException.java create mode 100755 core/src/com/cloud/exception/UnsupportedVersionException.java create mode 100644 core/src/com/cloud/ha/FenceBuilder.java create mode 100644 core/src/com/cloud/ha/HighAvailabilityManager.java create mode 100644 core/src/com/cloud/ha/Investigator.java create mode 100644 core/src/com/cloud/ha/WorkVO.java create mode 100644 core/src/com/cloud/ha/dao/HighAvailabilityDao.java create mode 100644 core/src/com/cloud/ha/dao/HighAvailabilityDaoImpl.java create mode 100644 core/src/com/cloud/host/DetailVO.java create mode 100755 core/src/com/cloud/host/Host.java create mode 100644 core/src/com/cloud/host/HostEnvironment.java create mode 100644 core/src/com/cloud/host/HostInfo.java create mode 100755 core/src/com/cloud/host/HostStats.java create mode 100644 core/src/com/cloud/host/HostVO.java create mode 100755 core/src/com/cloud/host/InsufficientServerCapacityException.java create mode 100644 core/src/com/cloud/host/Status.java create mode 100644 core/src/com/cloud/host/dao/DetailsDao.java create mode 100644 core/src/com/cloud/host/dao/DetailsDaoImpl.java create mode 100644 core/src/com/cloud/host/dao/HostDao.java create mode 100644 core/src/com/cloud/host/dao/HostDaoImpl.java create mode 100644 core/src/com/cloud/hypervisor/vmware/resource/VmwareResource.java create mode 100644 core/src/com/cloud/hypervisor/xen/resource/CitrixResourceBase.java create mode 100644 core/src/com/cloud/hypervisor/xen/resource/XcpServerResource.java create mode 100644 core/src/com/cloud/hypervisor/xen/resource/XenServerConnectionPool.java create mode 100644 core/src/com/cloud/info/ConsoleProxyConnectionInfo.java create mode 100644 core/src/com/cloud/info/ConsoleProxyInfo.java create mode 100644 core/src/com/cloud/info/ConsoleProxyLoadInfo.java create mode 100644 core/src/com/cloud/info/ConsoleProxyStatus.java create mode 100644 core/src/com/cloud/info/RunningHostCountInfo.java create mode 100644 core/src/com/cloud/info/RunningHostInfoAgregator.java create mode 100644 core/src/com/cloud/info/SecStorageVmLoadInfo.java create mode 100644 core/src/com/cloud/maid/StackMaid.java create mode 100644 core/src/com/cloud/maid/StackMaidVO.java create mode 100644 core/src/com/cloud/maid/dao/StackMaidDao.java create mode 100644 core/src/com/cloud/maid/dao/StackMaidDaoImpl.java create mode 100644 core/src/com/cloud/network/BasicVirtualNetworkAllocator.java create mode 100644 core/src/com/cloud/network/ExteralIpAddressAllocator.java create mode 100644 core/src/com/cloud/network/FirewallRuleVO.java create mode 100644 core/src/com/cloud/network/HAProxyConfigurator.java create mode 100644 core/src/com/cloud/network/IPAddressVO.java create mode 100644 core/src/com/cloud/network/IpAddrAllocator.java create mode 100644 core/src/com/cloud/network/LoadBalancerConfigurator.java create mode 100644 core/src/com/cloud/network/LoadBalancerVMMapVO.java create mode 100644 core/src/com/cloud/network/LoadBalancerVO.java create mode 100644 core/src/com/cloud/network/NetworkEnums.java create mode 100644 core/src/com/cloud/network/NetworkRuleConfigVO.java create mode 100644 core/src/com/cloud/network/SecurityGroupVMMapVO.java create mode 100644 core/src/com/cloud/network/SecurityGroupVO.java create mode 100644 core/src/com/cloud/network/VirtualNetworkAllocator.java create mode 100644 core/src/com/cloud/network/dao/FirewallRulesDao.java create mode 100644 core/src/com/cloud/network/dao/FirewallRulesDaoImpl.java create mode 100644 core/src/com/cloud/network/dao/IPAddressDao.java create mode 100644 core/src/com/cloud/network/dao/IPAddressDaoImpl.java create mode 100644 core/src/com/cloud/network/dao/LoadBalancerDao.java create mode 100644 core/src/com/cloud/network/dao/LoadBalancerDaoImpl.java create mode 100644 core/src/com/cloud/network/dao/LoadBalancerVMMapDao.java create mode 100644 core/src/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java create mode 100644 core/src/com/cloud/network/dao/NetworkRuleConfigDao.java create mode 100644 core/src/com/cloud/network/dao/NetworkRuleConfigDaoImpl.java create mode 100644 core/src/com/cloud/network/dao/SecurityGroupDao.java create mode 100644 core/src/com/cloud/network/dao/SecurityGroupDaoImpl.java create mode 100644 core/src/com/cloud/network/dao/SecurityGroupVMMapDao.java create mode 100644 core/src/com/cloud/network/dao/SecurityGroupVMMapDaoImpl.java create mode 100644 core/src/com/cloud/network/security/IngressRuleVO.java create mode 100644 core/src/com/cloud/network/security/NetworkGroupRulesVO.java create mode 100644 core/src/com/cloud/network/security/NetworkGroupVMMapVO.java create mode 100644 core/src/com/cloud/network/security/NetworkGroupVO.java create mode 100644 core/src/com/cloud/network/security/NetworkGroupWorkVO.java create mode 100644 core/src/com/cloud/network/security/VmRulesetLogVO.java create mode 100644 core/src/com/cloud/network/security/dao/IngressRuleDao.java create mode 100644 core/src/com/cloud/network/security/dao/IngressRuleDaoImpl.java create mode 100644 core/src/com/cloud/network/security/dao/NetworkGroupDao.java create mode 100644 core/src/com/cloud/network/security/dao/NetworkGroupDaoImpl.java create mode 100644 core/src/com/cloud/network/security/dao/NetworkGroupRulesDao.java create mode 100644 core/src/com/cloud/network/security/dao/NetworkGroupRulesDaoImpl.java create mode 100644 core/src/com/cloud/network/security/dao/NetworkGroupVMMapDao.java create mode 100644 core/src/com/cloud/network/security/dao/NetworkGroupVMMapDaoImpl.java create mode 100644 core/src/com/cloud/network/security/dao/NetworkGroupWorkDao.java create mode 100644 core/src/com/cloud/network/security/dao/NetworkGroupWorkDaoImpl.java create mode 100644 core/src/com/cloud/network/security/dao/VmRulesetLogDao.java create mode 100644 core/src/com/cloud/network/security/dao/VmRulesetLogDaoImpl.java create mode 100644 core/src/com/cloud/offerings/NetworkOfferingVO.java create mode 100644 core/src/com/cloud/resource/DiskPreparer.java create mode 100644 core/src/com/cloud/resource/NetworkPreparer.java create mode 100755 core/src/com/cloud/resource/ServerResource.java create mode 100755 core/src/com/cloud/resource/ServerResourceBase.java create mode 100644 core/src/com/cloud/serializer/GsonHelper.java create mode 100644 core/src/com/cloud/serializer/Param.java create mode 100644 core/src/com/cloud/serializer/SerializerHelper.java create mode 100644 core/src/com/cloud/server/Criteria.java create mode 100644 core/src/com/cloud/server/ManagementServer.java create mode 100644 core/src/com/cloud/service/ServiceOfferingVO.java create mode 100644 core/src/com/cloud/service/dao/ServiceOfferingDao.java create mode 100644 core/src/com/cloud/service/dao/ServiceOfferingDaoImpl.java create mode 100644 core/src/com/cloud/storage/DiskOfferingVO.java create mode 100755 core/src/com/cloud/storage/DiskTemplateVO.java create mode 100644 core/src/com/cloud/storage/FileSystemStorageResource.java create mode 100644 core/src/com/cloud/storage/GuestOS.java create mode 100644 core/src/com/cloud/storage/GuestOSCategoryVO.java create mode 100644 core/src/com/cloud/storage/GuestOSVO.java create mode 100644 core/src/com/cloud/storage/JavaStorageLayer.java create mode 100644 core/src/com/cloud/storage/LaunchPermissionVO.java create mode 100644 core/src/com/cloud/storage/SecondaryStorage.java create mode 100644 core/src/com/cloud/storage/SecondaryStorageLayer.java create mode 100644 core/src/com/cloud/storage/Snapshot.java create mode 100644 core/src/com/cloud/storage/SnapshotPolicyRefVO.java create mode 100644 core/src/com/cloud/storage/SnapshotPolicyVO.java create mode 100644 core/src/com/cloud/storage/SnapshotScheduleVO.java create mode 100644 core/src/com/cloud/storage/SnapshotVO.java create mode 100644 core/src/com/cloud/storage/StorageLayer.java create mode 100644 core/src/com/cloud/storage/StorageManager.java create mode 100644 core/src/com/cloud/storage/StoragePool.java create mode 100644 core/src/com/cloud/storage/StoragePoolDetailVO.java create mode 100644 core/src/com/cloud/storage/StoragePoolDiscoverer.java create mode 100644 core/src/com/cloud/storage/StoragePoolHostAssoc.java create mode 100644 core/src/com/cloud/storage/StoragePoolHostVO.java create mode 100644 core/src/com/cloud/storage/StoragePoolVO.java create mode 100755 core/src/com/cloud/storage/StorageResource.java create mode 100755 core/src/com/cloud/storage/StorageStats.java create mode 100644 core/src/com/cloud/storage/VMTemplateHostVO.java create mode 100644 core/src/com/cloud/storage/VMTemplateStoragePoolVO.java create mode 100644 core/src/com/cloud/storage/VMTemplateStorageResourceAssoc.java create mode 100644 core/src/com/cloud/storage/VMTemplateVO.java create mode 100644 core/src/com/cloud/storage/VMTemplateZoneVO.java create mode 100755 core/src/com/cloud/storage/VirtualMachineTemplate.java create mode 100755 core/src/com/cloud/storage/Volume.java create mode 100644 core/src/com/cloud/storage/VolumeStats.java create mode 100755 core/src/com/cloud/storage/VolumeVO.java create mode 100644 core/src/com/cloud/storage/dao/DiskOfferingDao.java create mode 100644 core/src/com/cloud/storage/dao/DiskOfferingDaoImpl.java create mode 100755 core/src/com/cloud/storage/dao/DiskTemplateDao.java create mode 100755 core/src/com/cloud/storage/dao/DiskTemplateDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/GuestOSCategoryDao.java create mode 100644 core/src/com/cloud/storage/dao/GuestOSCategoryDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/GuestOSDao.java create mode 100644 core/src/com/cloud/storage/dao/GuestOSDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/LaunchPermissionDao.java create mode 100644 core/src/com/cloud/storage/dao/LaunchPermissionDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/SnapshotDao.java create mode 100644 core/src/com/cloud/storage/dao/SnapshotDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/SnapshotPolicyDao.java create mode 100644 core/src/com/cloud/storage/dao/SnapshotPolicyDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/SnapshotPolicyRefDao.java create mode 100644 core/src/com/cloud/storage/dao/SnapshotPolicyRefDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/SnapshotScheduleDao.java create mode 100644 core/src/com/cloud/storage/dao/SnapshotScheduleDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/StoragePoolDao.java create mode 100644 core/src/com/cloud/storage/dao/StoragePoolDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/StoragePoolDetailsDao.java create mode 100644 core/src/com/cloud/storage/dao/StoragePoolDetailsDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/StoragePoolHostDao.java create mode 100644 core/src/com/cloud/storage/dao/StoragePoolHostDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/VMTemplateDao.java create mode 100644 core/src/com/cloud/storage/dao/VMTemplateDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/VMTemplateHostDao.java create mode 100644 core/src/com/cloud/storage/dao/VMTemplateHostDaoImpl.java create mode 100644 core/src/com/cloud/storage/dao/VMTemplatePoolDao.java create mode 100644 core/src/com/cloud/storage/dao/VMTemplatePoolDaoImpl.java create mode 100755 core/src/com/cloud/storage/dao/VMTemplateZoneDao.java create mode 100644 core/src/com/cloud/storage/dao/VMTemplateZoneDaoImpl.java create mode 100755 core/src/com/cloud/storage/dao/VolumeDao.java create mode 100755 core/src/com/cloud/storage/dao/VolumeDaoImpl.java create mode 100644 core/src/com/cloud/storage/preallocatedlun/PreallocatedLunDetailVO.java create mode 100644 core/src/com/cloud/storage/preallocatedlun/PreallocatedLunVO.java create mode 100644 core/src/com/cloud/storage/resource/DummySecondaryStorageResource.java create mode 100644 core/src/com/cloud/storage/resource/NfsSecondaryStorageResource.java create mode 100644 core/src/com/cloud/storage/resource/StoragePoolResource.java create mode 100644 core/src/com/cloud/storage/secondary/SecStorageVmAlertEventArgs.java create mode 100644 core/src/com/cloud/storage/secondary/SecondaryStorageVmAllocator.java create mode 100644 core/src/com/cloud/storage/secondary/SecondaryStorageVmManager.java create mode 100644 core/src/com/cloud/storage/template/DownloadManager.java create mode 100644 core/src/com/cloud/storage/template/DownloadManagerImpl.java create mode 100644 core/src/com/cloud/storage/template/HttpTemplateDownloader.java create mode 100644 core/src/com/cloud/storage/template/IsoProcessor.java create mode 100644 core/src/com/cloud/storage/template/LocalTemplateDownloader.java create mode 100644 core/src/com/cloud/storage/template/Processor.java create mode 100644 core/src/com/cloud/storage/template/QCOW2Processor.java create mode 100644 core/src/com/cloud/storage/template/ScpTemplateDownloader.java create mode 100644 core/src/com/cloud/storage/template/TemplateConstants.java create mode 100644 core/src/com/cloud/storage/template/TemplateDownloader.java create mode 100644 core/src/com/cloud/storage/template/TemplateDownloaderBase.java create mode 100644 core/src/com/cloud/storage/template/TemplateInfo.java create mode 100644 core/src/com/cloud/storage/template/TemplateLocation.java create mode 100644 core/src/com/cloud/storage/template/VhdProcessor.java create mode 100644 core/src/com/cloud/user/Account.java create mode 100644 core/src/com/cloud/user/AccountVO.java create mode 100644 core/src/com/cloud/user/User.java create mode 100644 core/src/com/cloud/user/UserAccount.java create mode 100644 core/src/com/cloud/user/UserAccountVO.java create mode 100644 core/src/com/cloud/user/UserContext.java create mode 100644 core/src/com/cloud/user/UserStatisticsVO.java create mode 100644 core/src/com/cloud/user/UserVO.java create mode 100644 core/src/com/cloud/user/dao/AccountDao.java create mode 100644 core/src/com/cloud/user/dao/AccountDaoImpl.java create mode 100644 core/src/com/cloud/user/dao/UserAccountDao.java create mode 100644 core/src/com/cloud/user/dao/UserAccountDaoImpl.java create mode 100644 core/src/com/cloud/user/dao/UserDao.java create mode 100644 core/src/com/cloud/user/dao/UserDaoImpl.java create mode 100644 core/src/com/cloud/user/dao/UserStatisticsDao.java create mode 100644 core/src/com/cloud/user/dao/UserStatisticsDaoImpl.java create mode 100644 core/src/com/cloud/vm/ConsoleProxy.java create mode 100644 core/src/com/cloud/vm/ConsoleProxyVO.java create mode 100755 core/src/com/cloud/vm/DomainRouter.java create mode 100755 core/src/com/cloud/vm/DomainRouterVO.java create mode 100644 core/src/com/cloud/vm/SecondaryStorageVm.java create mode 100644 core/src/com/cloud/vm/SecondaryStorageVmVO.java create mode 100755 core/src/com/cloud/vm/UserVmVO.java create mode 100644 core/src/com/cloud/vm/VMInstanceVO.java create mode 100644 core/src/com/cloud/vm/VirtualDisk.java create mode 100644 core/src/com/cloud/vm/VirtualEnvironment.java create mode 100644 core/src/com/cloud/vm/VirtualMachineManager.java create mode 100755 core/src/com/cloud/vm/VirtualMachineName.java create mode 100644 core/src/com/cloud/vm/VirtualNetwork.java create mode 100644 core/src/com/cloud/vm/VmStats.java create mode 100644 core/src/com/cloud/vm/dao/ConsoleProxyDao.java create mode 100644 core/src/com/cloud/vm/dao/ConsoleProxyDaoImpl.java create mode 100755 core/src/com/cloud/vm/dao/DomainRouterDao.java create mode 100755 core/src/com/cloud/vm/dao/DomainRouterDaoImpl.java create mode 100644 core/src/com/cloud/vm/dao/SecondaryStorageVmDao.java create mode 100644 core/src/com/cloud/vm/dao/SecondaryStorageVmDaoImpl.java create mode 100755 core/src/com/cloud/vm/dao/UserVmDao.java create mode 100755 core/src/com/cloud/vm/dao/UserVmDaoImpl.java create mode 100644 core/src/com/cloud/vm/dao/VMInstanceDao.java create mode 100644 core/src/com/cloud/vm/dao/VMInstanceDaoImpl.java create mode 100644 core/test/com/cloud/async/CleanupDelegate.java create mode 100644 core/test/com/cloud/async/TestAsync.java create mode 100644 core/test/com/cloud/vmware/TestVMWare.java create mode 100644 daemonize/COPYING create mode 100644 daemonize/daemonize.c create mode 100644 debian/README create mode 100644 debian/changelog create mode 100644 debian/cloud-agent-libs.install create mode 100644 debian/cloud-agent-scripts.config create mode 100644 debian/cloud-agent-scripts.install create mode 100644 debian/cloud-agent-scripts.postinst create mode 100644 debian/cloud-agent.config create mode 100644 debian/cloud-agent.install create mode 100644 debian/cloud-agent.postinst create mode 100644 debian/cloud-client-ui.install create mode 100644 debian/cloud-client.config create mode 100644 debian/cloud-client.install create mode 100644 debian/cloud-client.postinst create mode 100644 debian/cloud-console-proxy.config create mode 100644 debian/cloud-console-proxy.install create mode 100644 debian/cloud-console-proxy.postinst create mode 100644 debian/cloud-core.install create mode 100644 debian/cloud-daemonize.install create mode 100644 debian/cloud-deps.install create mode 100644 debian/cloud-management.config create mode 100644 debian/cloud-premium-deps.install create mode 100644 debian/cloud-premium.install create mode 100644 debian/cloud-python.install create mode 100644 debian/cloud-server.install create mode 100644 debian/cloud-setup.install create mode 100644 debian/cloud-test.install create mode 100644 debian/cloud-usage.install create mode 100644 debian/cloud-usage.postinst create mode 100644 debian/cloud-utils.install create mode 100644 debian/cloud-vnet.install create mode 100644 debian/cloud-vnet.postinst create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/dirs create mode 100755 debian/rules create mode 100644 deps/cloud-apache-log4j-extras-1.0.jar create mode 100644 deps/cloud-backport-util-concurrent-3.0.jar create mode 100644 deps/cloud-cglib.jar create mode 100644 deps/cloud-commons-codec-1.4.jar create mode 100644 deps/cloud-ehcache.jar create mode 100644 deps/cloud-email.jar create mode 100644 deps/cloud-gson-1.3.jar create mode 100644 deps/cloud-httpcore-4.0.jar create mode 100644 deps/cloud-jna.jar create mode 100644 deps/cloud-junit-4.8.1.jar create mode 100644 deps/cloud-libvirt-0.4.5.jar create mode 100644 deps/cloud-log4j.jar create mode 100644 deps/cloud-mysql-connector-java-5.1.7-bin.jar create mode 100644 deps/cloud-trilead-ssh2-build213.jar create mode 100644 deps/cloud-xenserver-5.5.0-1.jar create mode 100644 deps/cloud-xmlrpc-client-3.1.3.jar create mode 100644 deps/cloud-xmlrpc-common-3.1.3.jar create mode 100644 patches/.classpath create mode 100644 patches/.project create mode 100755 patches/kvm/etc/dnsmasq.conf create mode 100755 patches/kvm/etc/haproxy/haproxy.cfg create mode 100755 patches/kvm/etc/hosts create mode 100755 patches/kvm/etc/init.d/domr_webserver create mode 100755 patches/kvm/etc/init.d/seteth1 create mode 100755 patches/kvm/etc/init.d/vmops create mode 100755 patches/kvm/etc/rc.local create mode 100755 patches/kvm/etc/ssh/sshd_config create mode 100755 patches/kvm/etc/sysconfig/iptables create mode 100644 patches/kvm/etc/sysconfig/iptables-config create mode 100755 patches/kvm/etc/sysconfig/iptables-domp create mode 100755 patches/kvm/etc/sysconfig/iptables-domr create mode 100755 patches/kvm/etc/sysctl.conf create mode 100755 patches/kvm/root/edithosts.sh create mode 100755 patches/kvm/root/reconfigLB.sh create mode 100755 patches/kvm/root/run_domr_webserver create mode 100755 patches/kvm/root/send_password_to_domu.sh create mode 100644 patches/shared/var/www/html/latest/.htaccess create mode 100644 patches/shared/var/www/html/metadata/.htaccess create mode 100644 patches/shared/var/www/html/userdata/.htaccess create mode 100755 patches/xenserver/etc/init.d/postinit create mode 100755 patches/xenserver/etc/init.d/seteth1 create mode 100644 patches/xenserver/etc/sysconfig/iptables-config create mode 100644 patches/xenserver/etc/sysconfig/iptables-consoleproxy create mode 100644 patches/xenserver/etc/sysconfig/iptables-secstorage create mode 100644 patches/xenserver/etc/sysctl.conf create mode 100755 patches/xenserver/root/clearUsageRules.sh create mode 100755 patches/xenserver/root/edithosts.sh create mode 100755 patches/xenserver/root/firewall.sh create mode 100755 patches/xenserver/root/loadbalancer.sh create mode 100644 python/lib/cloud_PrettyPrint.py create mode 100644 python/lib/cloud_sxp.py create mode 100644 python/lib/cloud_utils.py create mode 100644 python/lib/cloud_utils.pyc create mode 100644 scripts/.classpath create mode 100644 scripts/.project create mode 100644 scripts/.pydevproject create mode 100755 scripts/installer/createtmplt.sh create mode 100755 scripts/installer/installcentos.sh create mode 100755 scripts/installer/installdomp.sh create mode 100755 scripts/installer/installrtr.sh create mode 100755 scripts/installer/run_installer.sh create mode 100755 scripts/network/domr/call_firewall.sh create mode 100755 scripts/network/domr/call_loadbalancer.sh create mode 100755 scripts/network/domr/dhcp_entry.sh create mode 100755 scripts/network/domr/firewall.sh create mode 100755 scripts/network/domr/firewall_vlan.sh create mode 100755 scripts/network/domr/ipassoc.sh create mode 100755 scripts/network/domr/ipassoc_vlan.sh create mode 100755 scripts/network/domr/loadbalancer.sh create mode 100755 scripts/network/domr/loadbalancer_vlan.sh create mode 100755 scripts/network/domr/save_password_to_domr.sh create mode 100755 scripts/network/domr/vm_data.sh create mode 100755 scripts/storage/checkchildren.sh create mode 100755 scripts/storage/installIso.sh create mode 100755 scripts/storage/qcow2/cleanupmyvms.sh create mode 100755 scripts/storage/qcow2/create_private_template.sh create mode 100755 scripts/storage/qcow2/createtmplt.sh create mode 100755 scripts/storage/qcow2/createvm.sh create mode 100755 scripts/storage/qcow2/delvm.sh create mode 100644 scripts/storage/qcow2/get_domr_kernel.sh create mode 100755 scripts/storage/qcow2/get_iqn.sh create mode 100755 scripts/storage/qcow2/importmpl.sh create mode 100755 scripts/storage/qcow2/listvmdisk.sh create mode 100755 scripts/storage/qcow2/listvmdisksize.sh create mode 100755 scripts/storage/qcow2/listvmtmplt.sh create mode 100755 scripts/storage/qcow2/managesnapshot.sh create mode 100644 scripts/storage/qcow2/managevolume.sh create mode 100644 scripts/storage/qcow2/modifyvlan.sh create mode 100755 scripts/storage/secondary/createtmplt.sh create mode 100755 scripts/storage/secondary/installIso.sh create mode 100755 scripts/storage/secondary/installrtng.sh create mode 100755 scripts/storage/secondary/listvmtmplt.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/createtmplt.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/createvm.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/delvm.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/filebacked/createtmplt.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/filebacked/createvm.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/filebacked/functions.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/filebacked/listvmdisk.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/filebacked/listvmdisksize.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/filebacked/migratetmplts.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/filebacked/upgradevmdisk.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/functions.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/host_group_destroy.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/listvmdisk.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/lu_info.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/lu_share.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/upgradevmdisk.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/view_and_lu_remove.sh create mode 100755 scripts/storage/zfs/iscsi/comstar/zfs_destroy.sh create mode 100755 scripts/storage/zfs/iscsi/create_private_template.sh create mode 100755 scripts/storage/zfs/iscsi/createdatadisk.sh create mode 100755 scripts/storage/zfs/iscsi/createtmplt.sh create mode 100755 scripts/storage/zfs/iscsi/createvm.sh create mode 100755 scripts/storage/zfs/iscsi/delvm.sh create mode 100755 scripts/storage/zfs/iscsi/get_iqn.sh create mode 100755 scripts/storage/zfs/iscsi/listvmdisk.sh create mode 100755 scripts/storage/zfs/iscsi/listvmdisksize.sh create mode 100755 scripts/storage/zfs/iscsi/listvmtmplt.sh create mode 100755 scripts/storage/zfs/iscsi/lu_info.sh create mode 100755 scripts/storage/zfs/iscsi/lu_share.sh create mode 100755 scripts/storage/zfs/iscsi/managesnapshot.sh create mode 100755 scripts/storage/zfs/iscsi/managevolume.sh create mode 100755 scripts/storage/zfs/iscsi/showdisks.sh create mode 100755 scripts/storage/zfs/iscsi/upgradevmdisk.sh create mode 100755 scripts/storage/zfs/nfs/createvm.sh create mode 100755 scripts/storage/zfs/nfs/delvm.sh create mode 100755 scripts/storage/zfs/nfs/listclones.sh create mode 100755 scripts/storage/zfs/nfs/listvmdisk.sh create mode 100755 scripts/storage/zfs/nfs/listvmdisksize.sh create mode 100755 scripts/storage/zfs/nfs/rundomr.sh create mode 100755 scripts/storage/zfs/nfs/runvm.sh create mode 100755 scripts/storage/zfs/nfs/stopvm.sh create mode 100755 scripts/storage/zfs/zfs_mount_recovery.sh create mode 100755 scripts/util/macgen.py create mode 100755 scripts/util/qemu-ifup create mode 100755 scripts/vm/hypervisor/kvm/rundomrpre.sh create mode 100755 scripts/vm/hypervisor/versions.sh create mode 100755 scripts/vm/hypervisor/xen/rebootvm.sh create mode 100755 scripts/vm/hypervisor/xen/rundomp.sh create mode 100755 scripts/vm/hypervisor/xen/rundomr.sh create mode 100755 scripts/vm/hypervisor/xen/rundomrpre.sh create mode 100755 scripts/vm/hypervisor/xen/runvm.sh create mode 100755 scripts/vm/hypervisor/xen/stopvm.sh create mode 100755 scripts/vm/hypervisor/xenserver/check_heartbeat.sh create mode 100755 scripts/vm/hypervisor/xenserver/find_bond.sh create mode 100644 scripts/vm/hypervisor/xenserver/hostvmstats.py create mode 100755 scripts/vm/hypervisor/xenserver/launch_hb.sh create mode 100755 scripts/vm/hypervisor/xenserver/make_migratable.sh create mode 100755 scripts/vm/hypervisor/xenserver/networkUsage.sh create mode 100755 scripts/vm/hypervisor/xenserver/network_info.sh create mode 100755 scripts/vm/hypervisor/xenserver/prepsystemvm.sh create mode 100755 scripts/vm/hypervisor/xenserver/setup_heartbeat_sr.sh create mode 100755 scripts/vm/hypervisor/xenserver/setup_iscsi.sh create mode 100755 scripts/vm/hypervisor/xenserver/setupxenserver.sh create mode 100755 scripts/vm/hypervisor/xenserver/vmops create mode 100755 scripts/vm/hypervisor/xenserver/vmopsSnapshot create mode 100755 scripts/vm/hypervisor/xenserver/xcpserver/NFSSR.py create mode 100755 scripts/vm/hypervisor/xenserver/xcpserver/cleanup.py create mode 100755 scripts/vm/hypervisor/xenserver/xcpserver/nfs.py create mode 100644 scripts/vm/hypervisor/xenserver/xcpserver/patch create mode 100755 scripts/vm/hypervisor/xenserver/xcpserver/scsiutil.py create mode 100755 scripts/vm/hypervisor/xenserver/xenheartbeat.sh create mode 100755 scripts/vm/hypervisor/xenserver/xenserver56/ISCSISR.py create mode 100755 scripts/vm/hypervisor/xenserver/xenserver56/LUNperVDI.py create mode 100755 scripts/vm/hypervisor/xenserver/xenserver56/NFSSR.py create mode 100755 scripts/vm/hypervisor/xenserver/xenserver56/cleanup.py create mode 100755 scripts/vm/hypervisor/xenserver/xenserver56/nfs.py create mode 100644 scripts/vm/hypervisor/xenserver/xenserver56/patch create mode 100755 scripts/vm/hypervisor/xenserver/xenserver56/scsiutil.py create mode 100755 scripts/vm/network/vnet/bridge.sh create mode 100755 scripts/vm/network/vnet/createvnet.sh create mode 100644 scripts/vm/network/vnet/modifyvlan.sh create mode 100755 scripts/vm/network/vnet/vnetcleanup.sh create mode 100755 scripts/vm/pingtest.sh create mode 100755 scripts/vm/storage/iscsi/comstar/iscsi_common.sh create mode 100755 scripts/vm/storage/iscsi/comstar/mapiscsi.sh create mode 100755 scripts/vm/storage/iscsi/comstar/mountrootdisk.sh create mode 100755 scripts/vm/storage/iscsi/comstar/mountvm.sh create mode 100755 scripts/vm/storage/iscsi/get_iqn.sh create mode 100755 scripts/vm/storage/iscsi/iscsi_common.sh create mode 100755 scripts/vm/storage/iscsi/iscsikill.sh create mode 100755 scripts/vm/storage/iscsi/iscsimon.sh create mode 100755 scripts/vm/storage/iscsi/mapiscsi.sh create mode 100755 scripts/vm/storage/iscsi/mirror.sh create mode 100755 scripts/vm/storage/iscsi/mirror_common.sh create mode 100755 scripts/vm/storage/iscsi/mountdatadisk.sh create mode 100755 scripts/vm/storage/iscsi/mountrootdisk.sh create mode 100755 scripts/vm/storage/iscsi/mountvm.sh create mode 100755 scripts/vm/storage/nfs/mountvm.sh create mode 100644 server/.classpath create mode 100644 server/.project create mode 100755 server/conf/log4j-cloud.xml.in create mode 100644 server/conf/migration-components.xml create mode 100755 server/scripts/vmops-fix-mysql-config create mode 100644 server/src/com/cloud/agent/manager/AgentAttache.java create mode 100755 server/src/com/cloud/agent/manager/AgentManagerImpl.java create mode 100755 server/src/com/cloud/agent/manager/AgentMonitor.java create mode 100644 server/src/com/cloud/agent/manager/ConnectedAgentAttache.java create mode 100644 server/src/com/cloud/agent/manager/DirectAgentAttache.java create mode 100644 server/src/com/cloud/agent/manager/DummyAttache.java create mode 100755 server/src/com/cloud/agent/manager/SynchronousListener.java create mode 100755 server/src/com/cloud/agent/manager/allocator/HostAllocator.java create mode 100755 server/src/com/cloud/agent/manager/allocator/PodAllocator.java create mode 100755 server/src/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java create mode 100644 server/src/com/cloud/agent/manager/allocator/impl/FirstFitRoutingAllocator.java create mode 100755 server/src/com/cloud/agent/manager/allocator/impl/RandomAllocator.java create mode 100644 server/src/com/cloud/agent/manager/allocator/impl/RecreateHostAllocator.java create mode 100755 server/src/com/cloud/agent/manager/allocator/impl/TestingAllocator.java create mode 100755 server/src/com/cloud/agent/manager/allocator/impl/UserConcentratedAllocator.java create mode 100644 server/src/com/cloud/alert/AlertManagerImpl.java create mode 100644 server/src/com/cloud/api/ApiServer.java create mode 100644 server/src/com/cloud/api/ApiServlet.java create mode 100644 server/src/com/cloud/api/BaseCmd.java create mode 100644 server/src/com/cloud/api/ServerApiException.java create mode 100644 server/src/com/cloud/api/commands/AddConfigCmd.java create mode 100644 server/src/com/cloud/api/commands/AddHostCmd.java create mode 100644 server/src/com/cloud/api/commands/AddSecondaryStorageCmd.java create mode 100644 server/src/com/cloud/api/commands/AssignPortForwardingServiceCmd.java create mode 100644 server/src/com/cloud/api/commands/AssignToLoadBalancerRuleCmd.java create mode 100644 server/src/com/cloud/api/commands/AssociateIPAddrCmd.java create mode 100644 server/src/com/cloud/api/commands/AttachIsoCmd.java create mode 100644 server/src/com/cloud/api/commands/AttachVolumeCmd.java create mode 100644 server/src/com/cloud/api/commands/AuthorizeNetworkGroupIngressCmd.java create mode 100644 server/src/com/cloud/api/commands/CancelMaintenanceCmd.java create mode 100644 server/src/com/cloud/api/commands/CancelPrimaryStorageMaintenanceCmd.java create mode 100644 server/src/com/cloud/api/commands/CopyIsoCmd.java create mode 100644 server/src/com/cloud/api/commands/CopyTemplateCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateDiskOfferingCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateDomainCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateIPForwardingRuleCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateLoadBalancerRuleCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateNetworkGroupCmd.java create mode 100644 server/src/com/cloud/api/commands/CreatePodCmd.java create mode 100644 server/src/com/cloud/api/commands/CreatePortForwardingServiceCmd.java create mode 100644 server/src/com/cloud/api/commands/CreatePortForwardingServiceRuleCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateServiceOfferingCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateSnapshotCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateSnapshotPolicyCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateStoragePoolCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateTemplateCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateUserCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateVlanIpRangeCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateVolumeCmd.java create mode 100644 server/src/com/cloud/api/commands/CreateZoneCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteDiskOfferingCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteDomainCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteHostCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteIPForwardingRuleCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteIsoCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteLoadBalancerRuleCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteNetworkGroupCmd.java create mode 100644 server/src/com/cloud/api/commands/DeletePodCmd.java create mode 100644 server/src/com/cloud/api/commands/DeletePoolCmd.java create mode 100644 server/src/com/cloud/api/commands/DeletePortForwardingServiceCmd.java create mode 100644 server/src/com/cloud/api/commands/DeletePortForwardingServiceRuleCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteServiceOfferingCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteSnapshotCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteSnapshotPoliciesCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteTemplateCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteUserCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteVlanIpRangeCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteVolumeCmd.java create mode 100644 server/src/com/cloud/api/commands/DeleteZoneCmd.java create mode 100644 server/src/com/cloud/api/commands/DeployVMCmd.java create mode 100644 server/src/com/cloud/api/commands/DestroyConsoleProxyCmd.java create mode 100644 server/src/com/cloud/api/commands/DestroyVMCmd.java create mode 100644 server/src/com/cloud/api/commands/DetachIsoCmd.java create mode 100644 server/src/com/cloud/api/commands/DetachVolumeCmd.java create mode 100644 server/src/com/cloud/api/commands/DisableAccountCmd.java create mode 100644 server/src/com/cloud/api/commands/DisableUserCmd.java create mode 100644 server/src/com/cloud/api/commands/DisassociateIPAddrCmd.java create mode 100644 server/src/com/cloud/api/commands/EnableAccountCmd.java create mode 100644 server/src/com/cloud/api/commands/EnableUserCmd.java create mode 100644 server/src/com/cloud/api/commands/GetCloudIdentifierCmd.java create mode 100644 server/src/com/cloud/api/commands/ListAccountsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListAlertsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListAsyncJobsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListCapacityCmd.java create mode 100644 server/src/com/cloud/api/commands/ListCfgsByCmd.java create mode 100644 server/src/com/cloud/api/commands/ListClustersCmd.java create mode 100644 server/src/com/cloud/api/commands/ListDiskOfferingsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListDomainChildrenCmd.java create mode 100644 server/src/com/cloud/api/commands/ListDomainsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListEventsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListGuestOsCategoriesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListGuestOsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListHostsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListIsoPermissionsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListIsosCmd.java create mode 100644 server/src/com/cloud/api/commands/ListLoadBalancerRuleInstancesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListLoadBalancerRulesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListNetworkGroupsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListPodsByCmd.java create mode 100644 server/src/com/cloud/api/commands/ListPortForwardingRulesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListPortForwardingServiceRulesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListPortForwardingServicesByVmCmd.java create mode 100644 server/src/com/cloud/api/commands/ListPortForwardingServicesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListPreallocatedLunsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListPublicIpAddressesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListRecurringSnapshotScheduleCmd.java create mode 100644 server/src/com/cloud/api/commands/ListResourceLimitsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListRoutersCmd.java create mode 100644 server/src/com/cloud/api/commands/ListServiceOfferingsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListSnapshotPoliciesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListSnapshotsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListStoragePoolsAndHostsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListStoragePoolsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListSystemVMsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListTemplateOrIsoPermissionsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListTemplatePermissionsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListTemplatesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListUsersCmd.java create mode 100644 server/src/com/cloud/api/commands/ListVMsCmd.java create mode 100644 server/src/com/cloud/api/commands/ListVlanIpRangesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListVolumesCmd.java create mode 100644 server/src/com/cloud/api/commands/ListZonesByCmd.java create mode 100644 server/src/com/cloud/api/commands/LockAccountCmd.java create mode 100644 server/src/com/cloud/api/commands/LockUserCmd.java create mode 100644 server/src/com/cloud/api/commands/PrepareForMaintenanceCmd.java create mode 100644 server/src/com/cloud/api/commands/PreparePrimaryStorageForMaintenanceCmd.java create mode 100644 server/src/com/cloud/api/commands/QueryAsyncJobResultCmd.java create mode 100644 server/src/com/cloud/api/commands/RebootRouterCmd.java create mode 100644 server/src/com/cloud/api/commands/RebootSystemVmCmd.java create mode 100644 server/src/com/cloud/api/commands/RebootVMCmd.java create mode 100644 server/src/com/cloud/api/commands/ReconnectHostCmd.java create mode 100644 server/src/com/cloud/api/commands/RecoverVMCmd.java create mode 100644 server/src/com/cloud/api/commands/RegisterCmd.java create mode 100644 server/src/com/cloud/api/commands/RegisterIsoCmd.java create mode 100644 server/src/com/cloud/api/commands/RegisterTemplateCmd.java create mode 100644 server/src/com/cloud/api/commands/RemoveFromLoadBalancerRuleCmd.java create mode 100644 server/src/com/cloud/api/commands/RemovePortForwardingServiceCmd.java create mode 100644 server/src/com/cloud/api/commands/ResetVMPasswordCmd.java create mode 100644 server/src/com/cloud/api/commands/RevokeNetworkGroupIngressCmd.java create mode 100644 server/src/com/cloud/api/commands/StartRouterCmd.java create mode 100644 server/src/com/cloud/api/commands/StartSystemVMCmd.java create mode 100644 server/src/com/cloud/api/commands/StartVMCmd.java create mode 100644 server/src/com/cloud/api/commands/StopRouterCmd.java create mode 100644 server/src/com/cloud/api/commands/StopSystemVmCmd.java create mode 100644 server/src/com/cloud/api/commands/StopVMCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateAccountCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateCfgCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateDiskOfferingCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateDomainCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateHostCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateIPForwardingRuleCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateIsoCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateIsoPermissionsCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateLoadBalancerRuleCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdatePodCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateResourceLimitCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateServiceOfferingCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateStoragePoolCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateTemplateCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateTemplateOrIsoPermissionsCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateTemplatePermissionsCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateUserCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateVMCmd.java create mode 100644 server/src/com/cloud/api/commands/UpdateZoneCmd.java create mode 100644 server/src/com/cloud/api/commands/UpgradeVMCmd.java create mode 100644 server/src/com/cloud/async/AsyncJobExecutor.java create mode 100644 server/src/com/cloud/async/AsyncJobExecutorContext.java create mode 100644 server/src/com/cloud/async/AsyncJobExecutorContextImpl.java create mode 100644 server/src/com/cloud/async/AsyncJobManager.java create mode 100644 server/src/com/cloud/async/AsyncJobManagerImpl.java create mode 100644 server/src/com/cloud/async/BaseAsyncJobExecutor.java create mode 100644 server/src/com/cloud/async/SyncQueueManager.java create mode 100644 server/src/com/cloud/async/SyncQueueManagerImpl.java create mode 100644 server/src/com/cloud/async/executor/AssignSecurityGroupExecutor.java create mode 100644 server/src/com/cloud/async/executor/AssignToLoadBalancerExecutor.java create mode 100644 server/src/com/cloud/async/executor/AssociateIpAddressExecutor.java create mode 100644 server/src/com/cloud/async/executor/AssociateIpAddressParam.java create mode 100644 server/src/com/cloud/async/executor/AssociateIpAddressResultObject.java create mode 100644 server/src/com/cloud/async/executor/AttachISOExecutor.java create mode 100644 server/src/com/cloud/async/executor/AttachISOParam.java create mode 100644 server/src/com/cloud/async/executor/AttachVolumeOperationResultObject.java create mode 100644 server/src/com/cloud/async/executor/AuthorizeNetworkGroupIngressExecutor.java create mode 100644 server/src/com/cloud/async/executor/CancelPrimaryStorageMaintenanceExecutor.java create mode 100644 server/src/com/cloud/async/executor/CompleteMaintenanceExecutor.java create mode 100644 server/src/com/cloud/async/executor/ConsoleProxyExecutorHelper.java create mode 100644 server/src/com/cloud/async/executor/ConsoleProxyOperationResultObject.java create mode 100644 server/src/com/cloud/async/executor/CopyTemplateExecutor.java create mode 100644 server/src/com/cloud/async/executor/CopyTemplateParam.java create mode 100644 server/src/com/cloud/async/executor/CopyTemplateResultObject.java create mode 100644 server/src/com/cloud/async/executor/CreateOrUpdateRuleExecutor.java create mode 100644 server/src/com/cloud/async/executor/CreateOrUpdateRuleParam.java create mode 100644 server/src/com/cloud/async/executor/CreateOrUpdateRuleResultObject.java create mode 100644 server/src/com/cloud/async/executor/CreatePrivateTemplateExecutor.java create mode 100644 server/src/com/cloud/async/executor/CreatePrivateTemplateParam.java create mode 100644 server/src/com/cloud/async/executor/CreatePrivateTemplateResultObject.java create mode 100644 server/src/com/cloud/async/executor/CreateSnapshotExecutor.java create mode 100644 server/src/com/cloud/async/executor/CreateSnapshotResultObject.java create mode 100644 server/src/com/cloud/async/executor/CreateVolumeFromSnapshotExecutor.java create mode 100644 server/src/com/cloud/async/executor/DeleteDomainExecutor.java create mode 100644 server/src/com/cloud/async/executor/DeleteDomainParam.java create mode 100644 server/src/com/cloud/async/executor/DeleteLoadBalancerExecutor.java create mode 100644 server/src/com/cloud/async/executor/DeleteNetworkRuleConfigExecutor.java create mode 100644 server/src/com/cloud/async/executor/DeleteRuleExecutor.java create mode 100644 server/src/com/cloud/async/executor/DeleteRuleParam.java create mode 100644 server/src/com/cloud/async/executor/DeleteSecurityGroupExecutor.java create mode 100644 server/src/com/cloud/async/executor/DeleteSnapshotExecutor.java create mode 100644 server/src/com/cloud/async/executor/DeleteTemplateExecutor.java create mode 100644 server/src/com/cloud/async/executor/DeleteTemplateParam.java create mode 100644 server/src/com/cloud/async/executor/DeleteUserExecutor.java create mode 100644 server/src/com/cloud/async/executor/DeployVMExecutor.java create mode 100644 server/src/com/cloud/async/executor/DeployVMParam.java create mode 100644 server/src/com/cloud/async/executor/DeployVMResultObject.java create mode 100644 server/src/com/cloud/async/executor/DestroyConsoleProxyExecutor.java create mode 100644 server/src/com/cloud/async/executor/DestroyVMExecutor.java create mode 100644 server/src/com/cloud/async/executor/DisableAccountExecutor.java create mode 100644 server/src/com/cloud/async/executor/DisableUserExecutor.java create mode 100644 server/src/com/cloud/async/executor/DisassociateIpAddressExecutor.java create mode 100644 server/src/com/cloud/async/executor/DisassociateIpAddressParam.java create mode 100644 server/src/com/cloud/async/executor/HostResultObject.java create mode 100644 server/src/com/cloud/async/executor/IngressRuleResultObject.java create mode 100644 server/src/com/cloud/async/executor/LoadBalancerParam.java create mode 100644 server/src/com/cloud/async/executor/NetworkGroupIngressParam.java create mode 100644 server/src/com/cloud/async/executor/NetworkGroupResultObject.java create mode 100644 server/src/com/cloud/async/executor/OperationResponse.java create mode 100644 server/src/com/cloud/async/executor/PrepareMaintenanceExecutor.java create mode 100644 server/src/com/cloud/async/executor/PreparePrimaryStorageMaintenanceExecutor.java create mode 100644 server/src/com/cloud/async/executor/PrimaryStorageResultObject.java create mode 100644 server/src/com/cloud/async/executor/RebootConsoleProxyExecutor.java create mode 100644 server/src/com/cloud/async/executor/RebootRouterExecutor.java create mode 100644 server/src/com/cloud/async/executor/RebootVMExecutor.java create mode 100644 server/src/com/cloud/async/executor/ReconnectExecutor.java create mode 100644 server/src/com/cloud/async/executor/RecurringSnapshotParam.java create mode 100644 server/src/com/cloud/async/executor/RemoveFromLoadBalancerExecutor.java create mode 100644 server/src/com/cloud/async/executor/RemoveSecurityGroupExecutor.java create mode 100644 server/src/com/cloud/async/executor/RemoveSecurityGroupParam.java create mode 100644 server/src/com/cloud/async/executor/ResetVMPasswordExecutor.java create mode 100644 server/src/com/cloud/async/executor/ResetVMPasswordParam.java create mode 100644 server/src/com/cloud/async/executor/RevokeNetworkGroupIngressExecutor.java create mode 100644 server/src/com/cloud/async/executor/RouterExecutorHelper.java create mode 100644 server/src/com/cloud/async/executor/RouterOperationResultObject.java create mode 100644 server/src/com/cloud/async/executor/SecurityGroupParam.java create mode 100644 server/src/com/cloud/async/executor/SnapshotOperationParam.java create mode 100644 server/src/com/cloud/async/executor/StartConsoleProxyExecutor.java create mode 100644 server/src/com/cloud/async/executor/StartConsoleProxyResult.java create mode 100644 server/src/com/cloud/async/executor/StartRouterExecutor.java create mode 100644 server/src/com/cloud/async/executor/StartRouterResultObject.java create mode 100644 server/src/com/cloud/async/executor/StartVMExecutor.java create mode 100644 server/src/com/cloud/async/executor/StopConsoleProxyExecutor.java create mode 100644 server/src/com/cloud/async/executor/StopRouterExecutor.java create mode 100644 server/src/com/cloud/async/executor/StopVMExecutor.java create mode 100644 server/src/com/cloud/async/executor/SystemVmCmdExecutor.java create mode 100644 server/src/com/cloud/async/executor/SystemVmOperationResultObject.java create mode 100644 server/src/com/cloud/async/executor/UpdateLoadBalancerParam.java create mode 100644 server/src/com/cloud/async/executor/UpdateLoadBalancerRuleExecutor.java create mode 100644 server/src/com/cloud/async/executor/UpdateLoadBalancerRuleResultObject.java create mode 100644 server/src/com/cloud/async/executor/UpdatePortForwardingRuleExecutor.java create mode 100644 server/src/com/cloud/async/executor/UpdatePortForwardingRuleResultObject.java create mode 100644 server/src/com/cloud/async/executor/UpgradeVMExecutor.java create mode 100644 server/src/com/cloud/async/executor/UpgradeVMParam.java create mode 100644 server/src/com/cloud/async/executor/VMExecutorHelper.java create mode 100644 server/src/com/cloud/async/executor/VMOperationExecutor.java create mode 100644 server/src/com/cloud/async/executor/VMOperationListener.java create mode 100644 server/src/com/cloud/async/executor/VMOperationParam.java create mode 100644 server/src/com/cloud/async/executor/VMOperationResultObject.java create mode 100644 server/src/com/cloud/async/executor/VmResultObject.java create mode 100644 server/src/com/cloud/async/executor/VolumeOperationExecutor.java create mode 100644 server/src/com/cloud/async/executor/VolumeOperationListener.java create mode 100644 server/src/com/cloud/async/executor/VolumeOperationParam.java create mode 100644 server/src/com/cloud/async/executor/VolumeOperationResultObject.java create mode 100644 server/src/com/cloud/cluster/DummyClusterManagerImpl.java create mode 100644 server/src/com/cloud/configuration/Config.java create mode 100644 server/src/com/cloud/configuration/ConfigurationManager.java create mode 100644 server/src/com/cloud/configuration/ConfigurationManagerImpl.java create mode 100644 server/src/com/cloud/consoleproxy/AgentBasedConsoleProxyManager.java create mode 100644 server/src/com/cloud/consoleproxy/AgentBasedStandaloneConsoleProxyManager.java create mode 100644 server/src/com/cloud/consoleproxy/ConsoleProxyBalanceAllocator.java create mode 100644 server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java create mode 100644 server/src/com/cloud/consoleproxy/StaticConsoleProxyManager.java create mode 100644 server/src/com/cloud/ha/CheckOnAgentInvestigator.java create mode 100644 server/src/com/cloud/ha/HighAvailabilityManagerImpl.java create mode 100644 server/src/com/cloud/ha/InvestigatorImpl.java create mode 100644 server/src/com/cloud/ha/StorageFence.java create mode 100644 server/src/com/cloud/ha/VmSyncListener.java create mode 100644 server/src/com/cloud/ha/XenServerInvestigator.java create mode 100644 server/src/com/cloud/hypervisor/vmware/discoverer/VmwareServerDiscoverer.java create mode 100644 server/src/com/cloud/hypervisor/xen/discoverer/XcpServerDiscoverer.java create mode 100644 server/src/com/cloud/maid/StackMaidManager.java create mode 100644 server/src/com/cloud/maid/StackMaidManagerImpl.java create mode 100644 server/src/com/cloud/maint/AgentUpgradeVO.java create mode 100644 server/src/com/cloud/maint/UpgradeManager.java create mode 100644 server/src/com/cloud/maint/UpgradeManagerImpl.java create mode 100644 server/src/com/cloud/maint/UpgradeManagerMBean.java create mode 100644 server/src/com/cloud/maint/UpgradeMonitor.java create mode 100644 server/src/com/cloud/maint/Version.java create mode 100644 server/src/com/cloud/maint/dao/AgentUpgradeDao.java create mode 100644 server/src/com/cloud/maint/dao/AgentUpgradeDaoImpl.java create mode 100644 server/src/com/cloud/migration/Db20to21MigrationUtil.java create mode 100644 server/src/com/cloud/migration/Db21to22MigrationUtil.java create mode 100644 server/src/com/cloud/migration/DiskOffering20Dao.java create mode 100644 server/src/com/cloud/migration/DiskOffering20DaoImpl.java create mode 100644 server/src/com/cloud/migration/DiskOffering20VO.java create mode 100644 server/src/com/cloud/migration/DiskOffering21Dao.java create mode 100644 server/src/com/cloud/migration/DiskOffering21DaoImpl.java create mode 100644 server/src/com/cloud/migration/DiskOffering21VO.java create mode 100644 server/src/com/cloud/migration/ServiceOffering20Dao.java create mode 100644 server/src/com/cloud/migration/ServiceOffering20DaoImpl.java create mode 100644 server/src/com/cloud/migration/ServiceOffering20VO.java create mode 100644 server/src/com/cloud/migration/ServiceOffering21Dao.java create mode 100644 server/src/com/cloud/migration/ServiceOffering21DaoImpl.java create mode 100644 server/src/com/cloud/migration/ServiceOffering21VO.java create mode 100644 server/src/com/cloud/network/NetworkManager.java create mode 100755 server/src/com/cloud/network/NetworkManagerImpl.java create mode 100644 server/src/com/cloud/network/NetworkProfileVO.java create mode 100644 server/src/com/cloud/network/SshKeysDistriMonitor.java create mode 100644 server/src/com/cloud/network/dao/NetworkProfileDao.java create mode 100644 server/src/com/cloud/network/dao/NetworkProfileDaoImpl.java create mode 100644 server/src/com/cloud/network/security/NetworkGroupListener.java create mode 100644 server/src/com/cloud/network/security/NetworkGroupManager.java create mode 100644 server/src/com/cloud/network/security/NetworkGroupManagerImpl.java create mode 100644 server/src/com/cloud/resource/Discoverer.java create mode 100644 server/src/com/cloud/resource/DiscovererBase.java create mode 100644 server/src/com/cloud/resource/DummyHostDiscoverer.java create mode 100644 server/src/com/cloud/resource/DummyHostServerResource.java create mode 100644 server/src/com/cloud/server/ConfigurationServer.java create mode 100644 server/src/com/cloud/server/ConfigurationServerImpl.java create mode 100755 server/src/com/cloud/server/ManagementServerImpl.java create mode 100644 server/src/com/cloud/server/StatsCollector.java create mode 100644 server/src/com/cloud/server/auth/DefaultUserAuthenticator.java create mode 100644 server/src/com/cloud/server/auth/MD5UserAuthenticator.java create mode 100644 server/src/com/cloud/server/auth/UserAuthenticator.java create mode 100644 server/src/com/cloud/servlet/CloudStartupServlet.java create mode 100644 server/src/com/cloud/servlet/ConsoleProxyServlet.java create mode 100644 server/src/com/cloud/storage/LocalStoragePoolListener.java create mode 100644 server/src/com/cloud/storage/StorageManagerImpl.java create mode 100644 server/src/com/cloud/storage/allocator/AbstractStoragePoolAllocator.java create mode 100644 server/src/com/cloud/storage/allocator/FirstFitStoragePoolAllocator.java create mode 100644 server/src/com/cloud/storage/allocator/GarbageCollectingStoragePoolAllocator.java create mode 100644 server/src/com/cloud/storage/allocator/LocalStoragePoolAllocator.java create mode 100644 server/src/com/cloud/storage/allocator/RandomStoragePoolAllocator.java create mode 100644 server/src/com/cloud/storage/allocator/StoragePoolAllocator.java create mode 100644 server/src/com/cloud/storage/allocator/UseLocalForRootAllocator.java create mode 100644 server/src/com/cloud/storage/download/DownloadAbandonedState.java create mode 100644 server/src/com/cloud/storage/download/DownloadActiveState.java create mode 100644 server/src/com/cloud/storage/download/DownloadCompleteState.java create mode 100644 server/src/com/cloud/storage/download/DownloadErrorState.java create mode 100644 server/src/com/cloud/storage/download/DownloadInProgressState.java create mode 100644 server/src/com/cloud/storage/download/DownloadInactiveState.java create mode 100644 server/src/com/cloud/storage/download/DownloadListener.java create mode 100644 server/src/com/cloud/storage/download/DownloadMonitor.java create mode 100644 server/src/com/cloud/storage/download/DownloadMonitorImpl.java create mode 100644 server/src/com/cloud/storage/download/DownloadState.java create mode 100644 server/src/com/cloud/storage/download/NotDownloadedState.java create mode 100755 server/src/com/cloud/storage/listener/StoragePoolMonitor.java create mode 100755 server/src/com/cloud/storage/listener/StorageSyncListener.java create mode 100644 server/src/com/cloud/storage/monitor/StorageHostMonitor.java create mode 100644 server/src/com/cloud/storage/preallocatedlun/dao/PreallocatedLunDao.java create mode 100644 server/src/com/cloud/storage/preallocatedlun/dao/PreallocatedLunDaoImpl.java create mode 100644 server/src/com/cloud/storage/preallocatedlun/dao/PreallocatedLunDetailsDao.java create mode 100644 server/src/com/cloud/storage/preallocatedlun/dao/PreallocatedLunDetailsDaoImpl.java create mode 100644 server/src/com/cloud/storage/secondary/LocalSecondaryStorageResource.java create mode 100644 server/src/com/cloud/storage/secondary/SecondaryStorageDiscoverer.java create mode 100644 server/src/com/cloud/storage/secondary/SecondaryStorageListener.java create mode 100644 server/src/com/cloud/storage/secondary/SecondaryStorageManagerImpl.java create mode 100644 server/src/com/cloud/storage/secondary/SecondaryStorageVmDefaultAllocator.java create mode 100644 server/src/com/cloud/storage/snapshot/SnapshotManager.java create mode 100644 server/src/com/cloud/storage/snapshot/SnapshotManagerImpl.java create mode 100644 server/src/com/cloud/storage/snapshot/SnapshotScheduler.java create mode 100644 server/src/com/cloud/storage/snapshot/SnapshotSchedulerImpl.java create mode 100644 server/src/com/cloud/template/TemplateManager.java create mode 100644 server/src/com/cloud/template/TemplateManagerImpl.java create mode 100644 server/src/com/cloud/test/DatabaseConfig.java create mode 100644 server/src/com/cloud/test/IPRangeConfig.java create mode 100644 server/src/com/cloud/test/PodZoneConfig.java create mode 100644 server/src/com/cloud/user/AccountManager.java create mode 100644 server/src/com/cloud/user/AccountManagerImpl.java create mode 100644 server/src/com/cloud/vm/MauriceMoss.java create mode 100644 server/src/com/cloud/vm/NicVO.java create mode 100644 server/src/com/cloud/vm/UserVmManager.java create mode 100755 server/src/com/cloud/vm/UserVmManagerImpl.java create mode 100644 server/src/com/cloud/vm/VmManager.java create mode 100644 server/test/async-job-component.xml create mode 100644 server/test/com/cloud/async/TestAsyncJobManager.java create mode 100644 server/test/com/cloud/async/TestSyncQueueManager.java create mode 100644 server/test/sync-queue-component.xml create mode 100644 setup/.project create mode 100644 setup/bindir/cloud-migrate-databases.in create mode 100755 setup/bindir/cloud-setup-databases.in create mode 100644 setup/db/create-database.sql create mode 100644 setup/db/create-index-fk.sql create mode 100644 setup/db/create-schema.sql create mode 100644 setup/db/data-20to21.sql create mode 100755 setup/db/deploy-db-dev.sh create mode 100644 setup/db/index-20to21.sql create mode 100644 setup/db/migration/schema-21to22.sql create mode 100644 setup/db/postprocess-20to21.sql create mode 100644 setup/db/schema-20to21.sql create mode 100644 setup/db/server-setup.sql create mode 100644 setup/db/server-setup.xml create mode 100644 setup/db/templates.kvm.sql create mode 100644 setup/db/templates.xenserver.sql create mode 100644 tools/.classpath create mode 100644 tools/.project create mode 100644 tools/ant/apache-ant-1.7.1/LICENSE create mode 100755 tools/ant/apache-ant-1.7.1/bin/ant create mode 100644 tools/ant/apache-ant-1.7.1/bin/ant.bat create mode 100644 tools/ant/apache-ant-1.7.1/bin/ant.cmd create mode 100755 tools/ant/apache-ant-1.7.1/bin/antRun create mode 100644 tools/ant/apache-ant-1.7.1/bin/antRun.bat create mode 100755 tools/ant/apache-ant-1.7.1/bin/antRun.pl create mode 100644 tools/ant/apache-ant-1.7.1/bin/antenv.cmd create mode 100755 tools/ant/apache-ant-1.7.1/bin/complete-ant-cmd.pl create mode 100644 tools/ant/apache-ant-1.7.1/bin/envset.cmd create mode 100644 tools/ant/apache-ant-1.7.1/bin/lcp.bat create mode 100755 tools/ant/apache-ant-1.7.1/bin/runant.pl create mode 100755 tools/ant/apache-ant-1.7.1/bin/runant.py create mode 100644 tools/ant/apache-ant-1.7.1/bin/runrc.cmd create mode 100644 tools/ant/apache-ant-1.7.1/etc/ant-bootstrap.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/README create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-antlr.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-apache-bcel.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-apache-bsf.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-apache-log4j.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-apache-oro.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-apache-regexp.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-apache-resolver.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-commons-logging.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-commons-net.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-jai.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-javamail.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-jdepend.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-jmf.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-jsch.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-junit.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-launcher.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-netrexx.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-nodeps.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-starteam.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-stylebook.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-swing.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-testutil.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-trax.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant-weblogic.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/ant.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/libraries.properties create mode 100644 tools/ant/apache-ant-1.7.1/lib/xercesImpl.jar create mode 100644 tools/ant/apache-ant-1.7.1/lib/xml-apis.jar create mode 100644 tools/gcc/README create mode 100644 tools/gcc/compiler.jar create mode 100644 tools/gcc/gcc.sh create mode 100644 ui/.classpath create mode 100644 ui/.project create mode 100644 ui/content/tab_accounts.html create mode 100644 ui/content/tab_configuration.html create mode 100644 ui/content/tab_dashboard.html create mode 100644 ui/content/tab_events.html create mode 100644 ui/content/tab_hosts.html create mode 100644 ui/content/tab_instances.html create mode 100644 ui/content/tab_networking.html create mode 100644 ui/content/tab_storage.html create mode 100644 ui/content/tab_templates.html create mode 100644 ui/css/cloud_custom.css create mode 100644 ui/css/images/._ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 ui/css/images/._ui-bg_flat_75_ffffff_40x100.png create mode 100644 ui/css/images/._ui-bg_glass_30_393939_1x400.png create mode 100644 ui/css/images/._ui-bg_glass_30_ffffff_1x400.png create mode 100644 ui/css/images/._ui-bg_glass_55_fbf9ee_1x400.png create mode 100644 ui/css/images/._ui-bg_inset-soft_95_fef1ec_1x100.png create mode 100644 ui/css/images/._ui-icons_222222_256x240.png create mode 100644 ui/css/images/._ui-icons_2e83ff_256x240.png create mode 100644 ui/css/images/._ui-icons_FFF_256x240.png create mode 100644 ui/css/images/._ui-icons_cd0a0a_256x240.png create mode 100644 ui/css/images/._ui-icons_ffffff_256x240.png create mode 100644 ui/css/images/alert_icon.png create mode 100644 ui/css/images/clr_button.gif create mode 100644 ui/css/images/clr_button_hover.gif create mode 100644 ui/css/images/comment_loader.gif create mode 100644 ui/css/images/grid_headerbg.gif create mode 100644 ui/css/images/groupbox_top.gif create mode 100644 ui/css/images/minimize_button.gif create mode 100644 ui/css/images/minimize_button_hover.gif create mode 100644 ui/css/images/play_button.gif create mode 100644 ui/css/images/play_button_hover.gif create mode 100644 ui/css/images/shrink_button.gif create mode 100644 ui/css/images/shrink_button_hover.gif create mode 100644 ui/css/images/sortable_groupbox.gif create mode 100644 ui/css/images/sortable_leftarrow.gif create mode 100644 ui/css/images/sortable_leftarrow_hover.gif create mode 100644 ui/css/images/sortable_rightarrow.gif create mode 100644 ui/css/images/sortable_rightarrow_hover.gif create mode 100644 ui/css/images/stop_button.gif create mode 100644 ui/css/images/stop_button_hover.gif create mode 100644 ui/css/images/ui-bg_errorglass_30_ffffff_1x400.png create mode 100644 ui/css/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 ui/css/images/ui-bg_flat_75_ffffff_40x100.png create mode 100644 ui/css/images/ui-bg_glass_30_393939_1x400.png create mode 100644 ui/css/images/ui-bg_glass_30_ffffff_1x400.png create mode 100644 ui/css/images/ui-bg_glass_55_fbf9ee_1x400.png create mode 100644 ui/css/images/ui-bg_glass_65_ffffff_1x400.png create mode 100644 ui/css/images/ui-bg_glass_75_000000_1x400.png create mode 100644 ui/css/images/ui-bg_glass_75_212121_1x400.png create mode 100644 ui/css/images/ui-bg_inset-soft_95_fef1ec_1x100.png create mode 100644 ui/css/images/ui-icons_222222_256x240.png create mode 100644 ui/css/images/ui-icons_2e83ff_256x240.png create mode 100644 ui/css/images/ui-icons_454545_256x240.png create mode 100644 ui/css/images/ui-icons_FFF_256x240.png create mode 100644 ui/css/images/ui-icons_cd0a0a_256x240.png create mode 100644 ui/css/images/ui-icons_ffffff_256x240.png create mode 100644 ui/css/jquery-ui-1.7.2.custom.css create mode 100644 ui/css/jquery-ui-1.8.2.custom.css create mode 100644 ui/css/logger.css create mode 100644 ui/css/main.css create mode 100644 ui/favicon.ico create mode 100644 ui/images/32bit_icon.gif create mode 100644 ui/images/64bit_icon.gif create mode 100644 ui/images/ISO_OFF.gif create mode 100644 ui/images/ISO_ON.gif create mode 100644 ui/images/KVM_icon.gif create mode 100644 ui/images/XEN_icon.gif create mode 100644 ui/images/account_bg.gif create mode 100644 ui/images/account_shadow.gif create mode 100644 ui/images/accountstitle_icons.gif create mode 100644 ui/images/add_domainbutton.gif create mode 100644 ui/images/add_domainbutton_hover.gif create mode 100644 ui/images/add_ipbutton.gif create mode 100644 ui/images/add_ipbutton_hover.gif create mode 100644 ui/images/add_userbutton.gif create mode 100644 ui/images/add_userbutton_hover.gif create mode 100644 ui/images/addnetgroup_button.gif create mode 100644 ui/images/addnetgroup_button_hover.gif create mode 100644 ui/images/admin_vmblue_left.gif create mode 100644 ui/images/admin_vmblue_mid.gif create mode 100644 ui/images/admin_vmblue_right.gif create mode 100644 ui/images/admin_vmconsole.gif create mode 100644 ui/images/admin_vmconsole_hover.gif create mode 100644 ui/images/admin_vmgrey_left.gif create mode 100644 ui/images/admin_vmgrey_mid.gif create mode 100644 ui/images/admin_vmgrey_right.gif create mode 100644 ui/images/admin_vmyellow_left.gif create mode 100644 ui/images/admin_vmyellow_mid.gif create mode 100644 ui/images/admin_vmyellow_right.gif create mode 100644 ui/images/adv_searchbutton.gif create mode 100644 ui/images/adv_searchbutton_hover.gif create mode 100644 ui/images/ajax-loader.gif create mode 100644 ui/images/alert_icon.png create mode 100644 ui/images/alerttitle_icons.gif create mode 100644 ui/images/alpha_rowbg.png create mode 100644 ui/images/alpha_vmrowbg.png create mode 100644 ui/images/arrow_bullet.gif create mode 100644 ui/images/bigrotation2.gif create mode 100644 ui/images/bootable_nonselectedicon.gif create mode 100644 ui/images/bootable_selectedicon.gif create mode 100644 ui/images/box_bullet.gif create mode 100644 ui/images/bytes_in.gif create mode 100644 ui/images/bytes_out.gif create mode 100644 ui/images/calendar_icon.gif create mode 100644 ui/images/capacitypanel_bot.gif create mode 100644 ui/images/capacitypanel_mid.gif create mode 100644 ui/images/capacitypanel_top.gif create mode 100644 ui/images/close_button.png create mode 100644 ui/images/close_button_hover.png create mode 100644 ui/images/cloud_logo.gif create mode 100644 ui/images/console_bg.png create mode 100644 ui/images/cproxytitle_icons.gif create mode 100644 ui/images/cpu_icon.gif create mode 100644 ui/images/crosszone_nonselectedicon.gif create mode 100644 ui/images/crosszone_selectedicon.gif create mode 100644 ui/images/dashboardtitle_icons.gif create mode 100644 ui/images/db_bardg_bg.gif create mode 100644 ui/images/db_bardg_titlebg.gif create mode 100644 ui/images/db_domain_top.gif create mode 100644 ui/images/db_domainbg.gif create mode 100644 ui/images/db_vmmid.gif create mode 100644 ui/images/db_vmyellowmid.gif create mode 100644 ui/images/details_downarrow.jpg create mode 100644 ui/images/details_uparrow.jpg create mode 100644 ui/images/disk_icon.gif create mode 100644 ui/images/diskofftitle_icons.gif create mode 100644 ui/images/display_boxbot.gif create mode 100644 ui/images/display_boxmid.gif create mode 100644 ui/images/display_boxtop.gif create mode 100644 ui/images/display_deleteicon.png create mode 100644 ui/images/display_deleteicon_hover.png create mode 100644 ui/images/display_editicon.png create mode 100644 ui/images/display_editicon_hover.png create mode 100644 ui/images/display_headerbg.gif create mode 100644 ui/images/display_rollbackicon.png create mode 100644 ui/images/display_rollbackicon_hover.png create mode 100644 ui/images/displaygrid_loader.gif create mode 100644 ui/images/domain_dbaccount.gif create mode 100644 ui/images/domain_dbdiskoff.gif create mode 100644 ui/images/domain_dbinstance.gif create mode 100644 ui/images/domain_dbsnapshots.gif create mode 100644 ui/images/domain_serachboxleft.gif create mode 100644 ui/images/domain_serachboxmid.gif create mode 100644 ui/images/domain_serachboxright.gif create mode 100644 ui/images/domaintitle_icons.gif create mode 100644 ui/images/domdetailsbox_left.gif create mode 100644 ui/images/domdetailsbox_mid.gif create mode 100644 ui/images/domdetailsbox_right.gif create mode 100644 ui/images/eventstitle_icons.gif create mode 100644 ui/images/featured_nonselectedicon.gif create mode 100644 ui/images/featured_selectedicon.gif create mode 100644 ui/images/graph_titleicon.gif create mode 100644 ui/images/green_statusbar.gif create mode 100644 ui/images/grey_statusbar.gif create mode 100644 ui/images/grid_headerbg create mode 100644 ui/images/grid_headerbg.gif create mode 100644 ui/images/grid_loader.gif create mode 100644 ui/images/grid_morebutton.gif create mode 100644 ui/images/grid_morebutton_hover.gif create mode 100644 ui/images/gridresult_closebutton.gif create mode 100644 ui/images/gridresult_closebutton_hover.gif create mode 100644 ui/images/gridsorting_downarrow.gif create mode 100644 ui/images/gridsorting_uparrow.gif create mode 100644 ui/images/group_icon.gif create mode 100644 ui/images/gsettingstitle_icons.gif create mode 100644 ui/images/ha_disable.gif create mode 100644 ui/images/ha_enable.gif create mode 100644 ui/images/header_bg create mode 100644 ui/images/header_bg.gif create mode 100644 ui/images/hostdetails_headerbg.jpg create mode 100644 ui/images/hostnetwork_icon.gif create mode 100644 ui/images/hosttitle_icons.gif create mode 100644 ui/images/hvm_nonselectedicon.gif create mode 100644 ui/images/hvm_selectedicon.gif create mode 100644 ui/images/instancetitle_icons.gif create mode 100644 ui/images/ip_ORicon.gif create mode 100644 ui/images/ip_detailtopbox.gif create mode 100644 ui/images/ip_managebox_bg.gif create mode 100644 ui/images/ip_managebox_icon.gif create mode 100644 ui/images/ip_searchbutton.gif create mode 100644 ui/images/ipbox_nonselected.gif create mode 100644 ui/images/ipbox_selected.gif create mode 100644 ui/images/ipdescr_boxbot.gif create mode 100644 ui/images/ipdescr_boxmid.gif create mode 100644 ui/images/ipdescr_boxtop.gif create mode 100644 ui/images/ipdescr_contbot.gif create mode 100644 ui/images/ipdescr_contbot_blank.gif create mode 100644 ui/images/ipdescr_contmid.gif create mode 100644 ui/images/ipdescr_contmid_blank.gif create mode 100644 ui/images/ipdescr_conttop.gif create mode 100644 ui/images/ipdescr_conttop_blank.gif create mode 100644 ui/images/iptitle_icons.gif create mode 100644 ui/images/isotitle_icons.gif create mode 100644 ui/images/laoding.gif create mode 100644 ui/images/load_add.gif create mode 100644 ui/images/load_addicon.gif create mode 100644 ui/images/load_aniloader.gif create mode 100644 ui/images/load_joint.gif create mode 100644 ui/images/load_routericon.gif create mode 100644 ui/images/load_stoppedvm.gif create mode 100644 ui/images/load_workingvm.gif create mode 100644 ui/images/loadgr_closebutton.png create mode 100644 ui/images/loadgr_closebutton_hover.png create mode 100644 ui/images/loadind_textbg.gif create mode 100644 ui/images/loading.gif create mode 100644 ui/images/loading_1.gif create mode 100644 ui/images/loading_addbg.gif create mode 100644 ui/images/loading_load.gif create mode 100644 ui/images/loading_messagebg.gif create mode 100644 ui/images/loadingmsg_left.gif create mode 100644 ui/images/loadingmsg_mid.gif create mode 100644 ui/images/loadingmsg_right.gif create mode 100644 ui/images/loadnetwork_titleicon.gif create mode 100644 ui/images/loadtitle_icons.gif create mode 100644 ui/images/loadvm_loader.gif create mode 100644 ui/images/logo.gif create mode 100644 ui/images/logout_bg.gif create mode 100644 ui/images/logout_bot.gif create mode 100644 ui/images/logout_cloudlogo.gif create mode 100644 ui/images/logout_logo.gif create mode 100644 ui/images/logout_mid.gif create mode 100644 ui/images/logout_top.gif create mode 100644 ui/images/mcontent_bg.gif create mode 100644 ui/images/memory_icon.gif create mode 100644 ui/images/overlay_morebot.png create mode 100644 ui/images/overlay_moremid.png create mode 100644 ui/images/overlay_moretop.png create mode 100644 ui/images/pagination_bg.gif create mode 100644 ui/images/pagination_firsticon.gif create mode 100644 ui/images/pagination_lasticon.gif create mode 100644 ui/images/pagination_nexticon.gif create mode 100644 ui/images/pagination_previcon.gif create mode 100644 ui/images/pagination_refresh.gif create mode 100644 ui/images/password_nonselectedicon.gif create mode 100644 ui/images/password_selectedicon.gif create mode 100644 ui/images/pin_icon.gif create mode 100644 ui/images/portnetwork_titleicon.gif create mode 100644 ui/images/primestoragetitle_icons.gif create mode 100644 ui/images/public_nonselectedicon.gif create mode 100644 ui/images/public_selectedicon.gif create mode 100644 ui/images/red_statusbar.gif create mode 100644 ui/images/register_box.gif create mode 100644 ui/images/rev_wizbot.gif create mode 100644 ui/images/rev_wizmid.gif create mode 100644 ui/images/rev_wiztop.jpg create mode 100644 ui/images/revwiz_backbutton.gif create mode 100644 ui/images/revwiz_closebutton.gif create mode 100644 ui/images/revwiz_closebutton_hover.gif create mode 100644 ui/images/revwiz_nextbutton.gif create mode 100644 ui/images/revwiz_nonselcted_tempbut.gif create mode 100644 ui/images/revwiz_nonselcted_tempbut_hover.gif create mode 100644 ui/images/revwiz_nonselectednumber.gif create mode 100644 ui/images/revwiz_selcted_tempbut.gif create mode 100644 ui/images/revwiz_selectednumber.gif create mode 100644 ui/images/routerstitle_icons.gif create mode 100644 ui/images/running_icon.gif create mode 100644 ui/images/safebar_bg.gif create mode 100644 ui/images/search_closearrow.gif create mode 100644 ui/images/search_resulticon.gif create mode 100644 ui/images/searchicon_button.jpg create mode 100644 ui/images/searchicon_button_hover.jpg create mode 100644 ui/images/searchpanel_bg.gif create mode 100644 ui/images/secondstoragetitle_icons.gif create mode 100644 ui/images/select_ipbg.gif create mode 100644 ui/images/select_ipbg_admin.gif create mode 100644 ui/images/serviceofftitle_icons.gif create mode 100644 ui/images/sgtitle_icons.gif create mode 100644 ui/images/small_ppbot.png create mode 100644 ui/images/small_ppmid.png create mode 100644 ui/images/small_pptop.png create mode 100644 ui/images/smenu_selectedbg.gif create mode 100644 ui/images/sprint.gif create mode 100644 ui/images/sprite1.gif create mode 100644 ui/images/srow_loading.png create mode 100644 ui/images/step1.png create mode 100644 ui/images/step2.png create mode 100644 ui/images/step3.png create mode 100644 ui/images/step4.png create mode 100644 ui/images/stepbox_slected.gif create mode 100644 ui/images/stopped_icon.gif create mode 100644 ui/images/stopped_vm.gif create mode 100644 ui/images/storagetitle_icons.gif create mode 100644 ui/images/submenu_bg.gif create mode 100644 ui/images/submenu_linkbg.gif create mode 100644 ui/images/temp_centosicon.gif create mode 100644 ui/images/temp_deleteicon.gif create mode 100644 ui/images/temp_deleteicon_hover.gif create mode 100644 ui/images/temp_editicon.gif create mode 100644 ui/images/temp_editicon_hover.gif create mode 100644 ui/images/temp_linuxicon.gif create mode 100644 ui/images/temp_windowsicon.gif create mode 100644 ui/images/templatestitle_icons.gif create mode 100644 ui/images/test_icon.gif create mode 100644 ui/images/tick_arrow.gif create mode 100644 ui/images/total_vm.gif create mode 100644 ui/images/tree_boxright.gif create mode 100644 ui/images/tree_eventicon.gif create mode 100644 ui/images/tree_minusopen_icon.png create mode 100644 ui/images/trusted_icon.gif create mode 100644 ui/images/unknown_icon.gif create mode 100644 ui/images/unsafebar_bg.gif create mode 100644 ui/images/untrusted_icon.gif create mode 100644 ui/images/userlinks_bgleft.gif create mode 100644 ui/images/userlinks_bgmid.gif create mode 100644 ui/images/userlinks_bgright.gif create mode 100644 ui/images/userlogin_mid.gif create mode 100644 ui/images/v1_logo.gif create mode 100644 ui/images/v1_popupbuttonbg.gif create mode 100644 ui/images/v1_popupbuttonbg_hover.gif create mode 100644 ui/images/v1_popupheaderbg.gif create mode 100644 ui/images/v1_vmopoup_left.png create mode 100644 ui/images/v1_vmopoup_mid.png create mode 100644 ui/images/v1_vmopoup_right.png create mode 100644 ui/images/v1adddiskoff_button.gif create mode 100644 ui/images/v1adddiskoff_button_hover.gif create mode 100644 ui/images/v1addhost_button.gif create mode 100644 ui/images/v1addhost_button_hover.gif create mode 100644 ui/images/v1addiso_button.gif create mode 100644 ui/images/v1addiso_button_hover.gif create mode 100644 ui/images/v1addloadbalncer_button.gif create mode 100644 ui/images/v1addloadbalncer_button_hover.gif create mode 100644 ui/images/v1addnewvm_button.gif create mode 100644 ui/images/v1addnewvm_button_hover.gif create mode 100644 ui/images/v1addpstorage_button.gif create mode 100644 ui/images/v1addpstorage_button_hover.gif create mode 100644 ui/images/v1addserviceoff_button.gif create mode 100644 ui/images/v1addserviceoff_button_hover.gif create mode 100644 ui/images/v1addsggroup_button.gif create mode 100644 ui/images/v1addsggroup_button_hover.gif create mode 100644 ui/images/v1addsstorage_button.gif create mode 100644 ui/images/v1addsstorage_button_hover.gif create mode 100644 ui/images/v1addtemplate_button.gif create mode 100644 ui/images/v1addtemplate_button_hover.gif create mode 100644 ui/images/v1addvolume_button.gif create mode 100644 ui/images/v1addvolume_button_hover.gif create mode 100644 ui/images/v1admin_menutab_off.gif create mode 100644 ui/images/v1admin_menutab_off_hover.gif create mode 100644 ui/images/v1admin_menutab_on.gif create mode 100644 ui/images/v1aquireip_button.gif create mode 100644 ui/images/v1aquireip_button_hover.gif create mode 100644 ui/images/v1grid_morebutton.gif create mode 100644 ui/images/v1grid_morebutton_hover.gif create mode 100644 ui/images/v1gridheader_bg.gif create mode 100644 ui/images/v1header_bg.gif create mode 100644 ui/images/v1menutab_off.gif create mode 100644 ui/images/v1menutab_off_hover.gif create mode 100644 ui/images/v1menutab_on.gif create mode 100644 ui/images/v1stepbox_slected.gif create mode 100644 ui/images/v1stepcontainer_bg.gif create mode 100644 ui/images/vm_actionbg.gif create mode 100644 ui/images/vm_continuebutton.gif create mode 100644 ui/images/vm_continuebutton_hover.gif create mode 100644 ui/images/vm_greenarrow.gif create mode 100644 ui/images/vm_greyarrow.gif create mode 100644 ui/images/vm_redarrow.gif create mode 100644 ui/images/vmbot_loader.gif create mode 100644 ui/images/vmdropdown_closebutt.gif create mode 100644 ui/images/vmdropdown_closebutt_hover.gif create mode 100644 ui/images/vmopoup_left.png create mode 100644 ui/images/vmopoup_mid.png create mode 100644 ui/images/vmopoup_right.png create mode 100644 ui/images/windowsconsole_icon.gif create mode 100644 ui/images/windowsconsole_iconhover.gif create mode 100644 ui/images/wizard_contentbg.gif create mode 100644 ui/images/working_vm.gif create mode 100644 ui/images/yellow_statusbar.gif create mode 100644 ui/images/zone_addicon.png create mode 100644 ui/images/zone_addipbutton.gif create mode 100644 ui/images/zone_addipbutton_hover.gif create mode 100644 ui/images/zone_addpodbutton.gif create mode 100644 ui/images/zone_addpodbutton_hover.gif create mode 100644 ui/images/zone_addpubIP.gif create mode 100644 ui/images/zone_addpubIP_hover.gif create mode 100644 ui/images/zone_changeipbutton.gif create mode 100644 ui/images/zone_changeipbutton_hover.gif create mode 100644 ui/images/zone_deletebutton.gif create mode 100644 ui/images/zone_deletebutton_hover.gif create mode 100644 ui/images/zone_detailsboxleft.gif create mode 100644 ui/images/zone_detailsboxmid.gif create mode 100644 ui/images/zone_detailsboxright.gif create mode 100644 ui/images/zone_directipbutton.gif create mode 100644 ui/images/zone_directipbutton_hover.gif create mode 100644 ui/images/zone_directipicon.png create mode 100644 ui/images/zone_editpodbutton.gif create mode 100644 ui/images/zone_editpodbutton_hover.gif create mode 100644 ui/images/zone_editzonebutton.gif create mode 100644 ui/images/zone_editzonebutton_hover.gif create mode 100644 ui/images/zone_enablefirewallbutton.gif create mode 100644 ui/images/zone_enablefirewallbutton_hover.gif create mode 100644 ui/images/zone_ipicon.png create mode 100644 ui/images/zone_openarrow.png create mode 100644 ui/images/zone_podicon.png create mode 100644 ui/images/zone_sidearrow.png create mode 100644 ui/images/zone_zoneicon.png create mode 100644 ui/images/zonedetails_left.gif create mode 100644 ui/images/zonedetails_mid.gif create mode 100644 ui/images/zonedetails_right.gif create mode 100644 ui/images/zonestitle_icons.gif create mode 100644 ui/images/zonetree_addbutton.jpg create mode 100644 ui/images/zonetree_blankbutton.jpg create mode 100644 ui/images/zonetree_left.gif create mode 100644 ui/images/zonetree_mid.gif create mode 100644 ui/images/zonetree_nonselectedbutton.jpg create mode 100644 ui/images/zonetree_right.gif create mode 100644 ui/images/zonetree_selectedbutton.jpg create mode 100644 ui/index.html create mode 100644 ui/jsp/tab_domains.jsp create mode 100644 ui/resources/resource.properties create mode 100644 ui/resources/resource_zh.properties create mode 100644 ui/scripts/cloud.core.accounts.js create mode 100644 ui/scripts/cloud.core.callbacks.js create mode 100644 ui/scripts/cloud.core.configuration.js create mode 100644 ui/scripts/cloud.core.domains.js create mode 100644 ui/scripts/cloud.core.events.js create mode 100644 ui/scripts/cloud.core.hosts.js create mode 100644 ui/scripts/cloud.core.init.js create mode 100644 ui/scripts/cloud.core.instances.js create mode 100644 ui/scripts/cloud.core.js create mode 100644 ui/scripts/cloud.core.network.js create mode 100644 ui/scripts/cloud.core.storage.js create mode 100644 ui/scripts/cloud.core.templates.js create mode 100644 ui/scripts/cloud.logger.js create mode 100644 ui/scripts/date.js create mode 100644 ui/scripts/jquery-1.4.2.min.js create mode 100644 ui/scripts/jquery-ui-1.8.2.custom.min.js create mode 100644 ui/scripts/jquery.cookies.js create mode 100644 ui/scripts/jquery.md5.js create mode 100644 ui/scripts/jquery.timers.js create mode 100644 ui/test/content/tab_test.html create mode 100644 ui/test/index.html create mode 100644 ui/test/scripts/cloud.core.test.js create mode 100644 utils/.classpath create mode 100644 utils/.project create mode 100644 utils/conf/db.properties create mode 100644 utils/conf/log4j-vmops.xml create mode 100644 utils/src/com/cloud/utils/ActionDelegate.java create mode 100644 utils/src/com/cloud/utils/DateUtil.java create mode 100644 utils/src/com/cloud/utils/EnumUtils.java create mode 100755 utils/src/com/cloud/utils/EnumerationImpl.java create mode 100644 utils/src/com/cloud/utils/IteratorUtil.java create mode 100755 utils/src/com/cloud/utils/NumbersUtil.java create mode 100755 utils/src/com/cloud/utils/Pair.java create mode 100644 utils/src/com/cloud/utils/PasswordGenerator.java create mode 100644 utils/src/com/cloud/utils/ProcessUtil.java create mode 100644 utils/src/com/cloud/utils/Profiler.java create mode 100755 utils/src/com/cloud/utils/PropertiesUtil.java create mode 100755 utils/src/com/cloud/utils/ReflectUtil.java create mode 100755 utils/src/com/cloud/utils/SerialVersionUID.java create mode 100644 utils/src/com/cloud/utils/StringUtils.java create mode 100644 utils/src/com/cloud/utils/Ternary.java create mode 100755 utils/src/com/cloud/utils/UUID.java create mode 100644 utils/src/com/cloud/utils/UriUtils.java create mode 100755 utils/src/com/cloud/utils/backoff/BackoffAlgorithm.java create mode 100755 utils/src/com/cloud/utils/backoff/impl/ConstantTimeBackoff.java create mode 100755 utils/src/com/cloud/utils/backoff/impl/ConstantTimeBackoffMBean.java create mode 100755 utils/src/com/cloud/utils/component/Adapter.java create mode 100644 utils/src/com/cloud/utils/component/AdapterBase.java create mode 100755 utils/src/com/cloud/utils/component/Adapters.java create mode 100755 utils/src/com/cloud/utils/component/ComponentLocator.java create mode 100755 utils/src/com/cloud/utils/component/ComponentLocatorMBean.java create mode 100644 utils/src/com/cloud/utils/component/Inject.java create mode 100755 utils/src/com/cloud/utils/component/Manager.java create mode 100644 utils/src/com/cloud/utils/concurrency/NamedThreadFactory.java create mode 100644 utils/src/com/cloud/utils/concurrency/Scheduler.java create mode 100644 utils/src/com/cloud/utils/concurrency/TestClock.java create mode 100755 utils/src/com/cloud/utils/db/Attribute.java create mode 100644 utils/src/com/cloud/utils/db/DB.java create mode 100644 utils/src/com/cloud/utils/db/DaoSearch.java create mode 100644 utils/src/com/cloud/utils/db/DataStore.java create mode 100644 utils/src/com/cloud/utils/db/DatabaseCallback.java create mode 100644 utils/src/com/cloud/utils/db/DatabaseCallbackFilter.java create mode 100755 utils/src/com/cloud/utils/db/DbUtil.java create mode 100755 utils/src/com/cloud/utils/db/Filter.java create mode 100755 utils/src/com/cloud/utils/db/GenericDao.java create mode 100755 utils/src/com/cloud/utils/db/GenericDaoBase.java create mode 100644 utils/src/com/cloud/utils/db/GenericSearchBuilder.java create mode 100644 utils/src/com/cloud/utils/db/GlobalLock.java create mode 100644 utils/src/com/cloud/utils/db/Merovingian.java create mode 100755 utils/src/com/cloud/utils/db/SearchBuilder.java create mode 100755 utils/src/com/cloud/utils/db/SearchCriteria.java create mode 100644 utils/src/com/cloud/utils/db/SequenceFetcher.java create mode 100755 utils/src/com/cloud/utils/db/SqlGenerator.java create mode 100755 utils/src/com/cloud/utils/db/Transaction.java create mode 100755 utils/src/com/cloud/utils/db/UpdateBuilder.java create mode 100755 utils/src/com/cloud/utils/encoding/Base64.java create mode 100644 utils/src/com/cloud/utils/events/EventArgs.java create mode 100644 utils/src/com/cloud/utils/events/EventsTest.java create mode 100644 utils/src/com/cloud/utils/events/SubscriptionMgr.java create mode 100755 utils/src/com/cloud/utils/exception/CloudRuntimeException.java create mode 100644 utils/src/com/cloud/utils/exception/ExceptionUtil.java create mode 100755 utils/src/com/cloud/utils/exception/ExecutionException.java create mode 100644 utils/src/com/cloud/utils/fsm/StateMachine.java create mode 100755 utils/src/com/cloud/utils/mgmt/JmxUtil.java create mode 100755 utils/src/com/cloud/utils/mgmt/ManagementBean.java create mode 100644 utils/src/com/cloud/utils/net/Ip4Address.java create mode 100755 utils/src/com/cloud/utils/net/MacAddress.java create mode 100755 utils/src/com/cloud/utils/net/NetUtils.java create mode 100644 utils/src/com/cloud/utils/net/NfsUtils.java create mode 100644 utils/src/com/cloud/utils/net/UrlUtil.java create mode 100755 utils/src/com/cloud/utils/nio/HandlerFactory.java create mode 100755 utils/src/com/cloud/utils/nio/Link.java create mode 100755 utils/src/com/cloud/utils/nio/NioClient.java create mode 100755 utils/src/com/cloud/utils/nio/NioConnection.java create mode 100755 utils/src/com/cloud/utils/nio/NioServer.java create mode 100755 utils/src/com/cloud/utils/nio/Task.java create mode 100644 utils/src/com/cloud/utils/script/Executor.java create mode 100755 utils/src/com/cloud/utils/script/OutputInterpreter.java create mode 100755 utils/src/com/cloud/utils/script/Script.java create mode 100644 utils/src/com/cloud/utils/script/ScriptBuilder.java create mode 100644 utils/src/com/cloud/utils/testcase/ComponentSetup.java create mode 100644 utils/src/com/cloud/utils/testcase/ComponentTestCase.java create mode 100644 utils/src/com/cloud/utils/testcase/Log4jEnabledTestCase.java create mode 100644 utils/src/com/cloud/utils/time/InaccurateClock.java create mode 100644 utils/src/javax/ejb/Local.java create mode 100644 utils/src/javax/persistence/AssociationOverride.java create mode 100644 utils/src/javax/persistence/AssociationOverrides.java create mode 100644 utils/src/javax/persistence/AttributeOverride.java create mode 100644 utils/src/javax/persistence/AttributeOverrides.java create mode 100644 utils/src/javax/persistence/Basic.java create mode 100644 utils/src/javax/persistence/CascadeType.java create mode 100644 utils/src/javax/persistence/Column.java create mode 100644 utils/src/javax/persistence/ColumnResult.java create mode 100644 utils/src/javax/persistence/DiscriminatorColumn.java create mode 100644 utils/src/javax/persistence/DiscriminatorType.java create mode 100644 utils/src/javax/persistence/DiscriminatorValue.java create mode 100644 utils/src/javax/persistence/Embeddable.java create mode 100644 utils/src/javax/persistence/Embedded.java create mode 100644 utils/src/javax/persistence/EmbeddedId.java create mode 100644 utils/src/javax/persistence/Entity.java create mode 100644 utils/src/javax/persistence/EntityExistsException.java create mode 100644 utils/src/javax/persistence/EntityListeners.java create mode 100755 utils/src/javax/persistence/EntityNotFoundException.java create mode 100644 utils/src/javax/persistence/EntityResult.java create mode 100755 utils/src/javax/persistence/EntityTransaction.java create mode 100644 utils/src/javax/persistence/EnumType.java create mode 100644 utils/src/javax/persistence/Enumerated.java create mode 100644 utils/src/javax/persistence/FetchType.java create mode 100644 utils/src/javax/persistence/FieldResult.java create mode 100644 utils/src/javax/persistence/FlushModeType.java create mode 100644 utils/src/javax/persistence/GeneratedValue.java create mode 100644 utils/src/javax/persistence/GenerationType.java create mode 100644 utils/src/javax/persistence/Id.java create mode 100644 utils/src/javax/persistence/IdClass.java create mode 100644 utils/src/javax/persistence/Inheritance.java create mode 100644 utils/src/javax/persistence/InheritanceType.java create mode 100644 utils/src/javax/persistence/JoinColumn.java create mode 100644 utils/src/javax/persistence/JoinColumns.java create mode 100644 utils/src/javax/persistence/JoinTable.java create mode 100644 utils/src/javax/persistence/Lob.java create mode 100644 utils/src/javax/persistence/LockModeType.java create mode 100644 utils/src/javax/persistence/ManyToMany.java create mode 100644 utils/src/javax/persistence/ManyToOne.java create mode 100644 utils/src/javax/persistence/MapKey.java create mode 100644 utils/src/javax/persistence/MappedSuperclass.java create mode 100644 utils/src/javax/persistence/OneToMany.java create mode 100644 utils/src/javax/persistence/OneToOne.java create mode 100644 utils/src/javax/persistence/OrderBy.java create mode 100644 utils/src/javax/persistence/PersistenceException.java create mode 100644 utils/src/javax/persistence/PostLoad.java create mode 100644 utils/src/javax/persistence/PostPersist.java create mode 100644 utils/src/javax/persistence/PostRemove.java create mode 100644 utils/src/javax/persistence/PostUpdate.java create mode 100644 utils/src/javax/persistence/PrimaryKeyJoinColumn.java create mode 100644 utils/src/javax/persistence/PrimaryKeyJoinColumns.java create mode 100644 utils/src/javax/persistence/SecondaryTable.java create mode 100644 utils/src/javax/persistence/SecondaryTables.java create mode 100644 utils/src/javax/persistence/SequenceGenerator.java create mode 100644 utils/src/javax/persistence/SqlResultSetMapping.java create mode 100644 utils/src/javax/persistence/SqlResultSetMappings.java create mode 100644 utils/src/javax/persistence/Table.java create mode 100644 utils/src/javax/persistence/TableGenerator.java create mode 100644 utils/src/javax/persistence/Temporal.java create mode 100644 utils/src/javax/persistence/TemporalType.java create mode 100644 utils/src/javax/persistence/Transient.java create mode 100644 utils/src/javax/persistence/UniqueConstraint.java create mode 100644 utils/src/javax/persistence/Version.java create mode 100644 utils/src/javax/persistence/package.html create mode 100644 utils/test/com/cloud/utils/TestProfiler.java create mode 100644 version-info.in create mode 100755 vnet/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-vnetd.in create mode 100755 vnet/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-vnetd.in create mode 100755 vnet/distro/ubuntu/SYSCONFDIR/init.d/cloud-vnetd.in create mode 100755 vnet/sbindir/cloud-vn.in create mode 100644 vnet/src/00INSTALL create mode 100644 vnet/src/00README create mode 100644 vnet/src/Make.env create mode 100644 vnet/src/Makefile create mode 100644 vnet/src/doc/Makefile create mode 100644 vnet/src/doc/man/vn.pod.1 create mode 100644 vnet/src/doc/vnet-module.txt create mode 100644 vnet/src/doc/vnet-xend.txt create mode 100644 vnet/src/examples/Makefile create mode 100755 vnet/src/examples/network-vnet create mode 100644 vnet/src/examples/vnet-insert create mode 100644 vnet/src/examples/vnet97.sxp create mode 100644 vnet/src/examples/vnet98.sxp create mode 100644 vnet/src/examples/vnet99.sxp create mode 100644 vnet/src/libxutil/Makefile create mode 100644 vnet/src/libxutil/allocate.c create mode 100644 vnet/src/libxutil/allocate.h create mode 100644 vnet/src/libxutil/debug.h create mode 100644 vnet/src/libxutil/enum.c create mode 100644 vnet/src/libxutil/enum.h create mode 100644 vnet/src/libxutil/fd_stream.c create mode 100644 vnet/src/libxutil/fd_stream.h create mode 100644 vnet/src/libxutil/file_stream.c create mode 100644 vnet/src/libxutil/file_stream.h create mode 100644 vnet/src/libxutil/gzip_stream.c create mode 100644 vnet/src/libxutil/gzip_stream.h create mode 100644 vnet/src/libxutil/hash_table.c create mode 100644 vnet/src/libxutil/hash_table.h create mode 100644 vnet/src/libxutil/iostream.c create mode 100644 vnet/src/libxutil/iostream.h create mode 100644 vnet/src/libxutil/kernel_stream.c create mode 100644 vnet/src/libxutil/kernel_stream.h create mode 100644 vnet/src/libxutil/lexis.c create mode 100644 vnet/src/libxutil/lexis.h create mode 100644 vnet/src/libxutil/mem_stream.c create mode 100644 vnet/src/libxutil/mem_stream.h create mode 100644 vnet/src/libxutil/socket_stream.c create mode 100644 vnet/src/libxutil/socket_stream.h create mode 100644 vnet/src/libxutil/string_stream.c create mode 100644 vnet/src/libxutil/string_stream.h create mode 100644 vnet/src/libxutil/sxpr.c create mode 100644 vnet/src/libxutil/sxpr.h create mode 100644 vnet/src/libxutil/sxpr_parser.c create mode 100644 vnet/src/libxutil/sxpr_parser.h create mode 100644 vnet/src/libxutil/sys_net.c create mode 100644 vnet/src/libxutil/sys_net.h create mode 100644 vnet/src/libxutil/sys_string.c create mode 100644 vnet/src/libxutil/sys_string.h create mode 100644 vnet/src/libxutil/util.c create mode 100644 vnet/src/libxutil/util.h create mode 100644 vnet/src/scripts/Makefile create mode 100644 vnet/src/scripts/vn create mode 100644 vnet/src/vnet-module/00README create mode 100644 vnet/src/vnet-module/Makefile create mode 100644 vnet/src/vnet-module/Makefile-2.4 create mode 100644 vnet/src/vnet-module/Makefile-2.6 create mode 100644 vnet/src/vnet-module/Makefile.ver create mode 100644 vnet/src/vnet-module/Makefile.vnet create mode 100644 vnet/src/vnet-module/esp.c create mode 100644 vnet/src/vnet-module/esp.h create mode 100644 vnet/src/vnet-module/etherip.c create mode 100644 vnet/src/vnet-module/etherip.h create mode 100644 vnet/src/vnet-module/if_etherip.h create mode 100644 vnet/src/vnet-module/if_varp.h create mode 100644 vnet/src/vnet-module/linux/pfkeyv2.h create mode 100644 vnet/src/vnet-module/random.c create mode 100644 vnet/src/vnet-module/random.h create mode 100644 vnet/src/vnet-module/sa.c create mode 100644 vnet/src/vnet-module/sa.h create mode 100644 vnet/src/vnet-module/sa_algorithm.c create mode 100644 vnet/src/vnet-module/sa_algorithm.h create mode 100644 vnet/src/vnet-module/skb_context.c create mode 100644 vnet/src/vnet-module/skb_context.h create mode 100644 vnet/src/vnet-module/skb_util.c create mode 100644 vnet/src/vnet-module/skb_util.h create mode 100644 vnet/src/vnet-module/sxpr_util.c create mode 100644 vnet/src/vnet-module/sxpr_util.h create mode 100644 vnet/src/vnet-module/timer_util.c create mode 100644 vnet/src/vnet-module/timer_util.h create mode 100644 vnet/src/vnet-module/tunnel.c create mode 100644 vnet/src/vnet-module/tunnel.h create mode 100644 vnet/src/vnet-module/varp.c create mode 100644 vnet/src/vnet-module/varp.h create mode 100644 vnet/src/vnet-module/varp_socket.c create mode 100644 vnet/src/vnet-module/varp_util.c create mode 100644 vnet/src/vnet-module/varp_util.h create mode 100644 vnet/src/vnet-module/vif.c create mode 100644 vnet/src/vnet-module/vif.h create mode 100644 vnet/src/vnet-module/vnet.c create mode 100644 vnet/src/vnet-module/vnet.h create mode 100644 vnet/src/vnet-module/vnet_dev.c create mode 100644 vnet/src/vnet-module/vnet_dev.h create mode 100644 vnet/src/vnet-module/vnet_eval.c create mode 100644 vnet/src/vnet-module/vnet_eval.h create mode 100644 vnet/src/vnet-module/vnet_forward.c create mode 100644 vnet/src/vnet-module/vnet_forward.h create mode 100644 vnet/src/vnet-module/vnet_ioctl.c create mode 100644 vnet/src/vnet-module/vnet_ioctl.h create mode 100644 vnet/src/vnetd/Makefile create mode 100644 vnet/src/vnetd/connection.c create mode 100644 vnet/src/vnetd/connection.h create mode 100644 vnet/src/vnetd/list.h create mode 100644 vnet/src/vnetd/select.c create mode 100644 vnet/src/vnetd/select.h create mode 100644 vnet/src/vnetd/selector.c create mode 100644 vnet/src/vnetd/selector.h create mode 100644 vnet/src/vnetd/skbuff.c create mode 100644 vnet/src/vnetd/skbuff.h create mode 100644 vnet/src/vnetd/spinlock.c create mode 100644 vnet/src/vnetd/spinlock.h create mode 100644 vnet/src/vnetd/sys_kernel.h create mode 100644 vnet/src/vnetd/timer.c create mode 100644 vnet/src/vnetd/timer.h create mode 100644 vnet/src/vnetd/vnetd.c create mode 100755 waf create mode 100755 waf.bat create mode 100644 wscript create mode 100644 wscript_build create mode 100644 wscript_configure diff --git a/HACKING b/HACKING new file mode 100644 index 00000000000..b6a16c3ef5e --- /dev/null +++ b/HACKING @@ -0,0 +1,652 @@ +--------------------------------------------------------------------- +THE QUICK GUIDE TO CLOUDSTACK DEVELOPMENT +--------------------------------------------------------------------- + + +=== Overview of the development lifecycle === + +To hack on a CloudStack component, you will generally: + +1. Configure the source code: + ./waf configure --prefix=/home/youruser/cloudstack + (see below, "./waf configure") + +2. Build and install the CloudStack + ./waf install + (see below, "./waf install") + +3. Set the CloudStack component up + (see below, "Running the CloudStack components from source") + +4. Run the CloudStack component + (see below, "Running the CloudStack components from source") + +5. Modify the source code + +6. Build and install the CloudStack again + ./waf install --preserve-config + (see below, "./waf install") + +7. GOTO 4 + + +=== What is this waf thing in my development lifecycle? === + +waf is a self-contained, advanced build system written by Thomas Nagy, +in the spirit of SCons or the GNU autotools suite. + +* To run waf on Linux / Mac: ./waf [...commands...] +* To run waf on Windows: waf.bat [...commands...] + +./waf --help should be your first discovery point to find out both the +configure-time options and the different processes that you can run +using waf. + + +=== What do the different waf commands above do? === + +1. ./waf configure --prefix=/some/path + + You run this command *once*, in preparation to building, or every + time you need to change a configure-time variable. + + This runs configure() in wscript, which takes care of setting the + variables and options that waf will use for compilation and + installation, including the installation directory (PREFIX). + + For convenience reasons, if you forget to run configure, waf + will proceed with some default configuration options. By + default, PREFIX is /usr/local, but you can set it e.g. to + /home/youruser/cloudstack if you plan to do a non-root + install. Be ware that you can later install the stack as a + regular user, but most components need to *run* as root. + + ./waf showconfig displays the values of the configure-time options + +2. ./waf + + You run this command to trigger compilation of the modified files. + + This runs the contents of wscript_build, which takes care of + discovering and describing what needs to be built, which + build products / sources need to be installed, and where. + +3. ./waf install + + You run this command when you want to install the CloudStack. + + If you are going to install for production, you should run this + process as root. If, conversely, you only want to install the + stack as your own user and in a directory that you have write + permission, it's fine to run waf install as your own user. + + This runs the contents of wscript_build, with an option variable + Options.is_install = True. When this variable is set, waf will + install the files described in wscript_build. For convenience + reasons, when you run install, any files that need to be recompiled + will also be recompiled prior to installation. + + -------------------- + + WARNING: each time you do ./waf install, the configuration files + in the installation directory are *overwritten*. + + There are, however, two ways to get around this: + + a) ./waf install has an option --preserve-config. If you pass + this option when installing, configuration files are never + overwritten. + + This option is useful when you have modified source files and + you need to deploy them on a system that already has the + CloudStack installed and configured, but you do *not* want to + overwrite the existing configuration of the CloudStack. + + If, however, you have reconfigured and rebuilt the source + since the last time you did ./waf install, then you are + advised to replace the configuration files and set the + components up again, because some configuration files + in the source use identifiers that may have changed during + the last ./waf configure. So, if this is your case, check + out the next way: + + b) Every configuration file can be overridden in the source + without touching the original. + + - Look for said config file X (or X.in) in the source, then + - create an override/ folder in the folder that contains X, then + - place a file named X (or X.in) inside override/, then + - put the desired contents inside X (or X.in) + + Now, every time you run ./waf install, the file that will be + installed is path/to/override/X.in, instead of /path/to/X.in. + + This option is useful if you are developing the CloudStack + and constantly reinstalling it. It guarantees that every + time you install the CloudStack, the installation will have + the correct configuration and will be ready to run. + + +=== Running the CloudStack components from source (for debugging / coding) === + +It is not technically possible to run the CloudStack components from +the source. That, however, is fine -- each component can be run +independently from the install directory: + +- Management Server + + 1) Execute ./waf install as your current user (or as root if the + installation path is only writable by root). + + WARNING: if any CloudStack configuration files have been + already configured / altered, they will be *overwritten* by this + process. Append --preserve-config to ./waf install to prevent this + from happening. Or resort to the override method discussed + above (search for "override" in this document). + + 2) If you haven't done so yet, set up the management server database: + + - either run ./waf deploydb_kvm, or + - run $BINDIR/cloud-setup-databases + + 3) Execute ./waf run as your current user (or as root if the + installation path is only writable by root). Alternatively, + you can use ./waf debug and this will run with debugging enabled. + + +- Agent (Linux-only): + + 1) Execute ./waf install as your current user (or as root if the + installation path is only writable by root). + + WARNING: if any CloudStack configuration files have been + already configured / altered, they will be *overwritten* by this + process. Append --preserve-config to ./waf install to prevent this + from happening. Or resort to the override method discussed + above (search for "override" in this document). + + 2) If you haven't done so yet, set the Agent up: + + - run $BINDIR/cloud-setup-agent + + 3) Execute ./waf run_agent as root + + this will launch sudo and require your root password unless you have + set sudo up not to ask for it + + +- Console Proxy (Linux-only): + + 1) Execute ./waf install as your current user (or as root if the + installation path is only writable by root). + + WARNING: if any CloudStack configuration files have been + already configured / altered, they will be *overwritten* by this + process. Append --preserve-config to ./waf install to prevent this + from happening. Or resort to the override method discussed + above (search for "override" in this document). + + 2) If you haven't done so yet, set the Console Proxy up: + + - run $BINDIR/cloud-setup-console-proxy + + 3) Execute ./waf run_console_proxy + + this will launch sudo and require your root password unless you have + set sudo up not to ask for it + + +--------------------------------------------------------------------- +BUILD SYSTEM TIPS +--------------------------------------------------------------------- + + +=== Integrating compilation and execution of each component into Eclipse === + +To run the Management Server from Eclipse, set up an External Tool of the +Program variety. Put the path to the waf binary in the Location of the +window, and the source directory as Working Directory. Then specify +"install --preserve-config run" as arguments (without the quotes). You can +now use the Run button in Eclipse to execute the Management Server directly +from Eclipse. You can replace run with debug if you want to run the +Management Server with the Debugging Proxy turned on. + +To run the Agent or Console Proxy from Eclipse, set up an External Tool of +the Program variety just like in the Management Server case. In there, +however, specify "install --preserve-config run_agent" or +"install --preserve-config run_console_proxy" as arguments instead. +Remember that you need to set sudo up to not ask you for a password and not +require a TTY, otherwise sudo -- implicitly called by waf run_agent or +waf run_console_proxy -- will refuse to work. + + +=== Building targets selectively === + +You can find out the targets of the build system: + +./waf list_targets + +If you want to run a specific task generator, + +./waf build --targets=patchsubst + +should run just that one (and whatever targets are required to build that +one, of course). + + +=== Common targets === + +* ./waf configure: you must always run configure once, and provide it with + the target installation paths for when you run install later + o --help: will show you all the configure options + o --no-dep-check: will skip dependency checks for java packages + needed to compile (saves 20 seconds when redoing the configure) + o --with-db-user, --with-db-pw, --with-db-host: informs the build + system of the MySQL configuration needed to set up the management + server upon install, and to do deploydb + +* ./waf build: will compile any source files (and, on some projects, will + also perform any variable substitutions on any .in files such as the + MANIFEST files). Build outputs will be in /artifacts/default. + +* ./waf install: will compile if not compiled yet, then execute an install + of the built targets. I had to write a significantly large amount of code + (that is, couple tens of lines of code) to make install work. + +* ./waf run: will run the management server in the foreground + +* ./waf debug: will run the management server in the foreground, and open + port 8787 to connect with the debugger (see the Run / debug options of + waf --help to change that port) + +* ./waf deploydb: deploys the database using the MySQL configuration supplied + with the configuration options when you did ./waf configure. RUN WAF BUILD + FIRST AT LEAST ONCE. + +* ./waf dist: create a source tarball. These tarballs will be distributed + independently on our Web site, and will form the source release of the + Cloud Stack. It is a self-contained release that can be ./waf built and + ./waf installed everywhere. + +* ./waf clean: remove known build products + +* ./waf distclean: remove the artifacts/ directory altogether + +* ./waf uninstall: uninstall all installed files + +* ./waf rpm: build RPM packages + o if the build fails because the system lacks dependencies from our + other modules, waf will attempt to install RPMs from the repos, + then try the build + o it will place the built packages in artifacts/rpmbuild/ + +* ./waf deb: build Debian packages + o if the build fails because the system lacks dependencies from our + other modules, waf will attempt to install DEBs from the repos, + then try the build + o it will place the built packages in artifacts/debbuild/ + +* ./waf uninstallrpms: removes all Cloud.com RPMs from a system (but not + logfiles or modified config files) + +* ./waf viewrpmdeps: displays RPM dependencies declared in the RPM specfile + +* ./waf installrpmdeps: runs Yum to install the packages required to build + the CloudStack + +* ./waf uninstalldebs: removes all Cloud.com DEBs from a system (AND logfiles + AND modified config files) +* ./waf viewdebdeps: displays DEB dependencies declared in the project + debian/control file + +* ./waf installdebdeps: runs aptitude to install the packages required to + build our software + + +=== Overriding certain source files === + +Earlier in this document we explored overriding configuration files. +Overrides are not limited to configuration files. + +If you want to provide your own server-setup.xml or SQL files in client/setup: + + * create a directory override inside the client/setup folder + * place your file that should override a file in client/setup there + +There's also override support in client/tomcatconf and agent/conf. + + +=== Environment substitutions === + +Any file named "something.in" has its tokens (@SOMETOKEN@) automatically +substituted for the corresponding build environment variable. The build +environment variables are generally constructed at configure time and +controllable by the --command-line-parameters to waf configure, and should +be available as a list of variables inside the file +artifacts/c4che/build.default.py. + + +=== The prerelease mechanism === + +The prerelease mechanism (--prerelease=BRANCHNAME) allows developers and +builders to build packages with pre-release Release tags. The Release tags +are constructed in such a way that both the build number and the branch name +is included, so developers can push these packages to repositories and upgrade +them using yum or aptitude without having to delete packages manually and +install packages manually every time a new build is done. Any package built +with the prerelease mechanism gets a standard X.Y.Z version number -- and, +due to the way that the prerelease Release tags are concocted, always upgrades +any older prerelease package already present on any system. The prerelease +mechanism must never be used to create packages that are intended to be +released as stable software to the general public. + +Relevant documentation: + + http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version + http://fedoraproject.org/wiki/PackageNamingGuidelines#Pre-Release_packages + +Everything comes together on the build server in the following way: + + +=== SCCS info === + +When building a source distribution (waf dist), or RPM/DEB distributions +(waf deb / waf rpm), waf will automatically detect the relevant source code +control information if the git command is present on the machine where waf +is run, and it will write the information to a file called sccs-info inside +the source tarball / install it into /usr/share/doc/cloud*/sccs-info when +installing the packages. + +If this source code conrol information cannot be calculated, then the old +sccs-info file is preserved across dist runs if it exists, and if it did +not exist before, the fact that the source could not be properly tracked +down to a repository is noted in the file. + + +=== Debugging the build system === + +Almost all targets have names. waf build -vvvvv --zones=task will give you +the task names that you can use in --targets. + + +--------------------------------------------------------------------- +UNDERSTANDING THE BUILD SYSTEM +--------------------------------------------------------------------- + + +=== Documentation for the build system === + +The first and foremost reference material: + +- http://freehackers.org/~tnagy/wafbook/index.html + +Examples + +- http://code.google.com/p/waf/wiki/CodeSnippets +- http://code.google.com/p/waf/w/list + +FAQ + +- http://code.google.com/p/waf/wiki/FAQ + + +=== Why waf === + +The CloudStack uses waf to build itself. waf is a relative newcomer +to the build system world; it borrows concepts from SCons and +other later-generation build systems: + +- waf is very flexible and rich; unlike other build systems, it covers + the entire life cycle, from compilation to installation to + uninstallation. it also supports dist (create source tarball), + distcheck (check that the source tarball compiles and installs), + autoconf-like checks for dependencies at compilation time, + and more. + +- waf is self-contained. A single file, distributed with the project, + enables everything to be built, with only a dependency on Python, + which is freely available and shipped in all Linux computers. + +- waf also supports building projects written in multiple languages + (in the case of the CloudStack, we build from C, Java and Python). + +- since waf is written in Python, the entire library of the Python + language is available to use in the build process. + + +=== Hacking on the build system: what are these wscript files? === + +1. wscript: contains most commands you can run from within waf +2. wscript_configure: contains the process that discovers the software + on the system and configures the build to fit that +2. wscript_build: contains a manifest of *what* is built and installed + +Refer to the waf book for general information on waf: + http://freehackers.org/~tnagy/wafbook/index.html + + +=== What happens when waf runs === + +When you run waf, this happens behind the scenes: + +- When you run waf for the first time, it unpacks itself to a hidden + directory .waf-1.X.Y.MD5SUM, including the main program and all + the Python libraries it provides and needs. + +- Immediately after unpacking itself, waf reads the wscript file + at the root of the source directory. After parsing this file and + loading the functions defined here, it reads wscript_build and + generates a function build() based on it. + +- After loading the build scripts as explained above, waf calls + the functions you specified in the command line. + +So, for example, ./waf configure build install will: + +* call configure() from wscript, +* call build() loaded from the contents of wscript_build, +* call build() once more but with Options.is_install = True. + +As part of build(), waf invokes ant to build the Java portion of our +stack. + + +=== How and why we use ant within waf === + +By now, you have probably noticed that we do, indeed, ship ant +build files in the CloudStack. During the build process, waf calls +ant directly to build the Java portions of our stack, and it uses +the resulting JAR files to perform the installation. + +The reason we do this rather than use the native waf capabilities +for building Java projects is simple: by using ant, we can leverage +the support built-in for ant in Eclipse and many other IDEs. Another +reason to do this is because Java developers are familiar with ant, +so adding a new JAR file or modifying what gets built into the +existing JAR files is facilitated for Java developers. + +If you add to the ant build files a new ant target that uses the +compile-java macro, waf will automatically pick it up, along with its +depends= and JAR name attributes. In general, all you need to do is +add the produced JAR name to the packaging manifests (cloud.spec and +debian/{name-of-package}.install). + + +--------------------------------------------------------------------- +FOR ANT USERS +--------------------------------------------------------------------- + + +If you are using Ant directly instead of using waf, these instructions apply to you: + +in this document, the example instructions are based on local source repository rooted at c:\root. You are free to locate it to anywhere you'd like to. +3.1 Setup developer build type + + 1) Go to c:\cloud\java\build directory + + 2) Copy file build-cloud.properties.template to file build-cloud.properties, then modify some of the parameters to match your local setup. The template properties file should have content as + + debug=true + debuglevel=lines,vars,source + tomcat.home=$TOMCAT_HOME --> change to your local Tomcat root directory such as c:/apache-tomcat-6.0.18 + debug.jvmarg=-Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n + deprecation=off + build.type=developer + target.compat.version=1.5 + source.compat.version=1.5 + branding.name=default + + 3) Make sure the following Environment variables and Path are set: + +set enviroment variables: +CATALINA_HOME: +JAVA_HOME: +CLOUD_HOME: +MYSQL_HOME: + +update the path to include + +MYSQL_HOME\bin + + 4) Clone a full directory tree of C:\cloud\java\build\deploy\production to C:\cloud\java\build\deploy\developer + + You can use Windows Explorer to copy the directory tree over. Please note, during your daily development process, whenever you see updates in C:\cloud\java\build\deploy\production, be sure to sync it into C:\cloud\java\build\deploy\developer. +3.2 Common build instructions + +After you have setup the build type, you are ready to perform build and run Management Server alone locally. + +cd java +python waf configure build install + +More at Build system. + +Will install the management server and its requisites to the appropriate place (your Tomcat instance on Windows, /usr/local on Linux). It will also install the agent to /usr/local/cloud/agent (this will change in the future). +4. Database and Server deployment + +After a successful management server build (database deployment scripts use some of the artifacts from build process), you can use database deployment script to deploy and initialize the database. You can find the deployment scripts in C:/cloud/java/build/deploy/db. deploy-db.sh is used to create, populate your DB instance. Please take a look at content of deploy-db.sh for more details + +Before you run the scripts, you should edit C:/cloud/java/build/deploy/developer/db/server-setup-dev.xml to allocate Public and Private IP ranges for your development setup. Ensure that the ranges you pick are unallocated to others. + +Customized VM templates to be populated are in C:/cloud/java/build/deploy/developer/db/templates-dev.sql Edit this file to customize the templates to your needs. + +Deploy the DB by running + +./deploy-db.sh ../developer/db/server-setup-dev.xml ../developer/db/templates-dev.xml +4.1. Management Server Deployment + +ant build-server + +Build Management Server + +ant deploy-server + +Deploy Management Server software to Tomcat environment + +ant debug + +Start Management Server in debug mode. The JVM debug options can be found in cloud-build.properties + +ant run + +Start Management Server in normal mode. + +5. Agent deployment + +After a successful build process, you should be able to find build artifacts at distribution directory, in this example case, for developer build type, the artifacts locate at c:\cloud\java\dist\developer, particularly, if you have run + +ant package-agent build command, you should see the agent software be packaged in a single file named agent.zip under c:\cloud\java\dist\developer, together with the agent deployment script deploy-agent.sh. +5.1 Agent Type + +Agent software can be deployed and configured to serve with different roles at run time. In current implementation, there are 3 types of agent configuration, respectively called as Computing Server, Routing Server and Storage Server. + + * When agent software is configured to run as Computing server, it is responsible to host user VMs. Agent software should be running in Xen Dom0 system on computer server machine. + + * When agent software is configured to run as Routing Server, it is responsible to host routing VMs for user virtual network and console proxy system VMs. Routing server serves as the bridge to outside network, the machine that agent software is running should have at least two network interfaces, one towards outside network, one participates the internal VMOps management network. Like computer server, agent software on routing server should also be running in Xen Dom0 system. + + * When agent software is configured to run as Storage server, it is responsible to provide storage service for all VMs. The storage service is based on ZFS running on a Solaris system, agent software on storage server is therefore running under Solaris (actually a Solaris VM), Dom0 systems on computing server and routing server can access the storage service through iScsi initiator. The storage volume will be eventually mounted on Dom0 system and make available to DomU VMs through our agent software. + +5.2 Resource sharing + +All developers can share the same set of agent server machines for development, to make this possible, the concept of instance appears in various places + + * VM names. VM names are structual names, it contains a instance section that can identify VMs from different VMOps cloud instances. VMOps cloud instance name is configured in server configuration parameter AgentManager/instance.name + * iScsi initiator mount point. For Computing servers and Routing servers, the mount point can distinguish the mounted DomU VM images from different agent deployments. The mount location can be specified in agent.properties file with a name-value pair named mount.parent + * iScsi target allocation point. For storage servers, this allocation point can distinguish the storage allocation from different storage agent deployments. The allocation point can be specified in agent.properties file with a name-value pair named parent + +5.4 Deploy agent software + +Before running the deployment scripts, first copy the build artifacts agent.zip and deploy-agent.sh to your personal development directory on agent server machines. By our current convention, you can create your personal development directory that usually locates at /root/your name. In following example, the agent package and deployment scripts are copied to test0.lab.vmops.com and the deployment script file has been marked as executible. + + On build machine, + + scp agent.zip root@test0:/root/your name + + scp deploy-agent.sh root@test0:/root/your name + + On agent server machine + +chmod +x deploy-agent.sh +5.4.1 Deploy agent on computing server + +deploy-agent.sh -d /root//agent -h -t computing -m expert +5.4.2 Deploy agent on routing server + +deploy-agent.sh -d /root//agent -h -t routing -m expert +5.4.3 Deploy agent on storage server + +deploy-agent.sh -d /root//agent -h -t storage -m expert +5.5 Configure agent + +After you have deployed the agent software, you should configure the agent by editing the agent.properties file under /root//agent/conf directory on each of the Routing, Computing and Storage servers. Add/Edit following properties. The rest are defaults that get populated by the agent at runtime. + workers=3 + host= + port=8250 + pod= + zone= + instance= + developer=true + +Following is a sample agent.properties file for Routing server + + workers=3 + id=1 + port=8250 + pod=RC + storage=comstar + zone=RC + type=routing + private.network.nic=xenbr0 + instance=RC + public.network.nic=xenbr1 + developer=true + host=192.168.1.138 +5.5 Running agent + +Edit /root//agent/conf/log4j-cloud.xml to update the location of logs to somewhere under /root/ + +Once you have deployed and configured the agent software, you are ready to launch it. Under the agent root directory (in our example, /root//agent. there is a scrip file named run.sh, you can use it to launch the agent. + +Launch agent in detached background process + +nohup ./run.sh & + +Launch agent in interactive mode + +./run.sh + +Launch agent in debug mode, for example, following command makes JVM listen at TCP port 8787 + +./run.sh -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n + +If agent is launched in debug mode, you may use Eclipse IDE to remotely debug it, please note, when you are sharing agent server machine with others, choose a TCP port that is not in use by someone else. + +Please also note that, run.sh also searches for /etc/cloud directory for agent.properties, make sure it uses the correct agent.properties file! +5.5. Stopping the Agents + +the pid of the agent process is in /var/run/agent..pid + +To Stop the agent: + +kill + + \ No newline at end of file diff --git a/INSTALL b/INSTALL new file mode 100644 index 00000000000..bcf10e20b23 --- /dev/null +++ b/INSTALL @@ -0,0 +1,155 @@ +--------------------------------------------------------------------- +TABLE OF CONTENTS +--------------------------------------------------------------------- + + +1. Really quick start: building and installing a production stack +2. Post-install: setting the CloudStack components up +3. Installation paths: where the stack is installed on your system +4. Uninstalling the CloudStack from your system + + +--------------------------------------------------------------------- +REALLY QUICK START: BUILDING AND INSTALLING A PRODUCTION STACK +--------------------------------------------------------------------- + + +You have two options. Choose one: + +a) Building distribution packages from the source and installing them +b) Building from the source and installing directly from there + + +=== I want to build and install distribution packages === + +This is the recommended way to run your CloudStack cloud. The +advantages are that dependencies are taken care of automatically +for you, and you can verify the integrity of the installed files +using your system's package manager. + +1. As root, install the build dependencies. + + a) Fedora / CentOS: ./waf installrpmdeps + + b) Ubuntu: ./waf installdebdeps + +2. As a non-root user, build the CloudStack packages. + + a) Fedora / CentOS: ./waf rpm + + b) Ubuntu: ./waf deb + +3. As root, install the CloudStack packages. + You can choose which components to install on your system. + + a) Fedora / CentOS: the installable RPMs are in artifacts/rpmbuild + install as root: rpm -ivh artifacts/rpmbuild/RPMS/{x86_64,noarch,i386}/*.rpm + + b) Ubuntu: the installable DEBs are in artifacts/debbuild + install as root: dpkg -i artifacts/debbuild/*.deb + +4. Configure and start the components you intend to run. + Consult the Installation Guide to find out how to + configure each component, and "Installation paths" for information + on where programs, initscripts and config files are installed. + + +=== I want to build and install directly from the source === + +This is the recommended way to run your CloudStack cloud if you +intend to modify the source, if you intend to port the CloudStack to +another distribution, or if you intend to run the CloudStack on a +distribution for which packages are not built. + +1. As root, install the build dependencies. + See below for a list. + +2. As non-root, configure the build. + See below to discover configuration options. + + ./waf configure + +3. As non-root, build the CloudStack. + To learn more, see "Quick guide to developing, building and + installing from source" below. + + ./waf build + +4. As root, install the runtime dependencies. + See below for a list. + +5. As root, Install the CloudStack + + ./waf install + +6. Configure and start the components you intend to run. + Consult the Installation Guide to find out how to + configure each component, and "Installation paths" for information + on where to find programs, initscripts and config files mentioned + in the Installation Guide (paths may vary). + + +=== Dependencies of the CloudStack === + +- Build dependencies: + + 1. FIXME DEPENDENCIES LIST THEM HERE + +- Runtime dependencies: + + 2. FIXME DEPENDENCIES LIST THEM HERE + + +--------------------------------------------------------------------- +INSTALLATION PATHS: WHERE THE STACK IS INSTALLED ON YOUR SYSTEM +--------------------------------------------------------------------- + + +The CloudStack build system installs files on a variety of paths, each +one of which is selectable when building from source. + +- $PREFIX: + the default prefix where the entire stack is installed + defaults to /usr/local on source builds + defaults to /usr on package builds + +- $SYSCONFDIR/cloud: + + the prefix for CloudStack configuration files + defaults to $PREFIX/etc/cloud on source builds + defaults to /etc/cloud on package builds + +- $SYSCONFDIR/init.d: + the prefix for CloudStack initscripts + defaults to $PREFIX/etc/init.d on source builds + defaults to /etc/init.d on package builds + +- $BINDIR: + the CloudStack installs programs there + defaults to $PREFIX/bin on source builds + defaults to /usr/bin on package builds + +- $LIBEXECDIR: + the CloudStack installs service runners there + defaults to $PREFIX/libexec on source builds + defaults to /usr/libexec on package builds (/usr/bin on Ubuntu) + + +--------------------------------------------------------------------- +UNINSTALLING THE CLOUDSTACK FROM YOUR SYSTEM +--------------------------------------------------------------------- + + +- If you installed the CloudStack using packages, use your operating + system package manager to remove the CloudStack packages. + + a) Fedora / CentOS: the installable RPMs are in artifacts/rpmbuild + as root: rpm -qa | grep ^cloud- | xargs rpm -e + + b) Ubuntu: the installable DEBs are in artifacts/debbuild + aptitude purge '~ncloud' + +- If you installed from a source tree: + + ./waf uninstall + diff --git a/README b/README new file mode 100644 index 00000000000..b0478ff475f --- /dev/null +++ b/README @@ -0,0 +1,52 @@ +Hello, and thanks for downloading the Cloud.com CloudStackâ„¢! The +Cloud.com CloudStackâ„¢ is Open Source Software that allows +organizations to build Infrastructure as a Service (Iaas) clouds. +Working with server, storage, and networking equipment of your +choice, the CloudStack provides a turn-key software stack that +dramatically simplifies the process of deploying and managing a +cloud. + + +--------------------------------------------------------------------- +HOW TO INSTALL THE CLOUDSTACK +--------------------------------------------------------------------- + + +Please refer to the document INSTALL distributed with the source. + + +--------------------------------------------------------------------- +HOW TO HACK ON THE CLOUDSTACK +--------------------------------------------------------------------- + + +Please refer to the document HACKING distributed with the source. + + +--------------------------------------------------------------------- +BE PART OF THE CLOUD.COM COMMUNITY! +--------------------------------------------------------------------- + + +We are more than happy to have you ask us questions, hack our source +code, and receive your contributions. + +* Our forums are available at http://cloud.com/community . +* If you would like to modify / extend / hack on the CloudStack source, + refer to the file HACKING for more information. +* If you find bugs, please log on to http://bugs.cloud.com/ and file + a report. +* If you have patches to send us get in touch with us at info@cloud.com + or file them as attachments in our bug tracker above. + + +--------------------------------------------------------------------- +Cloud.com's contact information is: + +20400 Stevens Creek Blvd +Suite 390 +Cupertino, CA 95014 +Tel: +1 (888) 384-0962 + +This software is OSI certified Open Source Software. OSI Certified is a +certification mark of the Open Source Initiative. diff --git a/README.html b/README.html new file mode 100644 index 00000000000..2ece7a070e7 --- /dev/null +++ b/README.html @@ -0,0 +1,10555 @@ + + + + + + + + + + + + + Cloud.com CloudStack - Documentation + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
<!--{{{-->
+<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
+<!--}}}-->
+
+
+
+
Background: #fff
+Foreground: #000
+PrimaryPale: #8cf
+PrimaryLight: #18f
+PrimaryMid: #04b
+PrimaryDark: #014
+SecondaryPale: #ffc
+SecondaryLight: #fe8
+SecondaryMid: #db4
+SecondaryDark: #841
+TertiaryPale: #eee
+TertiaryLight: #ccc
+TertiaryMid: #999
+TertiaryDark: #666
+Error: #f88
+
+
+
+
/*{{{*/
+body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+
+a {color:[[ColorPalette::PrimaryMid]];}
+a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
+a img {border:0;}
+
+h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
+h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
+h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}
+
+.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
+.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
+.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}
+
+.header {background:[[ColorPalette::PrimaryMid]];}
+.headerShadow {color:[[ColorPalette::Foreground]];}
+.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
+.headerForeground {color:[[ColorPalette::Background]];}
+.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}
+
+.tabSelected{color:[[ColorPalette::PrimaryDark]];
+	background:[[ColorPalette::TertiaryPale]];
+	border-left:1px solid [[ColorPalette::TertiaryLight]];
+	border-top:1px solid [[ColorPalette::TertiaryLight]];
+	border-right:1px solid [[ColorPalette::TertiaryLight]];
+}
+.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
+.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
+.tabContents .button {border:0;}
+
+#sidebar {}
+#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
+#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}
+
+.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
+.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
+.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
+.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
+	border:1px solid [[ColorPalette::PrimaryMid]];}
+.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
+.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
+.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
+.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
+	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
+.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
+.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
+	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}
+
+.wizard .notChanged {background:transparent;}
+.wizard .changedLocally {background:#80ff80;}
+.wizard .changedServer {background:#8080ff;}
+.wizard .changedBoth {background:#ff8080;}
+.wizard .notFound {background:#ffff80;}
+.wizard .putToServer {background:#ff80ff;}
+.wizard .gotFromServer {background:#80ffff;}
+
+#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
+#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}
+
+.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}
+
+.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
+.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
+.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
+.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
+.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
+.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
+.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}
+
+.tiddler .defaultCommand {font-weight:bold;}
+
+.shadow .title {color:[[ColorPalette::TertiaryDark]];}
+
+.title {color:[[ColorPalette::SecondaryDark]];}
+.subtitle {color:[[ColorPalette::TertiaryDark]];}
+
+.toolbar {color:[[ColorPalette::PrimaryMid]];}
+.toolbar a {color:[[ColorPalette::TertiaryLight]];}
+.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
+.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}
+
+.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
+.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
+.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
+.tagging .button, .tagged .button {border:none;}
+
+.footer {color:[[ColorPalette::TertiaryLight]];}
+.selected .footer {color:[[ColorPalette::TertiaryMid]];}
+
+.sparkline {background:[[ColorPalette::PrimaryPale]]; border:0;}
+.sparktick {background:[[ColorPalette::PrimaryDark]];}
+
+.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
+.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
+.lowlight {background:[[ColorPalette::TertiaryLight]];}
+
+.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}
+
+.imageLink, #displayArea .imageLink {background:transparent;}
+
+.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}
+
+.viewer .listTitle {list-style-type:none; margin-left:-2em;}
+.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
+.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}
+
+.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
+.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
+.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}
+
+.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
+.viewer code {color:[[ColorPalette::SecondaryDark]];}
+.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}
+
+.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}
+
+.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
+.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
+.editorFooter {color:[[ColorPalette::TertiaryMid]];}
+.readOnly {background:[[ColorPalette::TertiaryPale]];}
+
+#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
+#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
+#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
+#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
+#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
+#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
+.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
+.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
+#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:'alpha(opacity=60)';}
+/*}}}*/
+
+
+
/*{{{*/
+* html .tiddler {height:1%;}
+
+body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}
+
+h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
+h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
+h4,h5,h6 {margin-top:1em;}
+h1 {font-size:1.35em;}
+h2 {font-size:1.25em;}
+h3 {font-size:1.1em;}
+h4 {font-size:1em;}
+h5 {font-size:.9em;}
+
+hr {height:1px;}
+
+a {text-decoration:none;}
+
+dt {font-weight:bold;}
+
+ol {list-style-type:decimal;}
+ol ol {list-style-type:lower-alpha;}
+ol ol ol {list-style-type:lower-roman;}
+ol ol ol ol {list-style-type:decimal;}
+ol ol ol ol ol {list-style-type:lower-alpha;}
+ol ol ol ol ol ol {list-style-type:lower-roman;}
+ol ol ol ol ol ol ol {list-style-type:decimal;}
+
+.txtOptionInput {width:11em;}
+
+#contentWrapper .chkOptionInput {border:0;}
+
+.externalLink {text-decoration:underline;}
+
+.indent {margin-left:3em;}
+.outdent {margin-left:3em; text-indent:-3em;}
+code.escaped {white-space:nowrap;}
+
+.tiddlyLinkExisting {font-weight:bold;}
+.tiddlyLinkNonExisting {font-style:italic;}
+
+/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
+a.tiddlyLinkNonExisting.shadow {font-weight:bold;}
+
+#mainMenu .tiddlyLinkExisting,
+	#mainMenu .tiddlyLinkNonExisting,
+	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
+#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}
+
+.header {position:relative;}
+.header a:hover {background:transparent;}
+.headerShadow {position:relative; padding:4.5em 0 1em 1em; left:-1px; top:-1px;}
+.headerForeground {position:absolute; padding:4.5em 0 1em 1em; left:0px; top:0px;}
+
+.siteTitle {font-size:3em;}
+.siteSubtitle {font-size:1.2em;}
+
+#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}
+
+#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
+#sidebarOptions {padding-top:0.3em;}
+#sidebarOptions a {margin:0 0.2em; padding:0.2em 0.3em; display:block;}
+#sidebarOptions input {margin:0.4em 0.5em;}
+#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
+#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
+#sidebarOptions .sliderPanel input {margin:0 0 0.3em 0;}
+#sidebarTabs .tabContents {width:15em; overflow:hidden;}
+
+.wizard {padding:0.1em 1em 0 2em;}
+.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
+.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
+.wizardStep {padding:1em 1em 1em 1em;}
+.wizard .button {margin:0.5em 0 0; font-size:1.2em;}
+.wizardFooter {padding:0.8em 0.4em 0.8em 0;}
+.wizardFooter .status {padding:0 0.4em; margin-left:1em;}
+.wizard .button {padding:0.1em 0.2em;}
+
+#messageArea {position:fixed; top:2em; right:0; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
+.messageToolbar {display:block; text-align:right; padding:0.2em;}
+#messageArea a {text-decoration:underline;}
+
+.tiddlerPopupButton {padding:0.2em;}
+.popupTiddler {position: absolute; z-index:300; padding:1em; margin:0;}
+
+.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
+.popup .popupMessage {padding:0.4em;}
+.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0;}
+.popup li.disabled {padding:0.4em;}
+.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
+.listBreak {font-size:1px; line-height:1px;}
+.listBreak div {margin:2px 0;}
+
+.tabset {padding:1em 0 0 0.5em;}
+.tab {margin:0 0 0 0.25em; padding:2px;}
+.tabContents {padding:0.5em;}
+.tabContents ul, .tabContents ol {margin:0; padding:0;}
+.txtMainTab .tabContents li {list-style:none;}
+.tabContents li.listLink { margin-left:.75em;}
+
+#contentWrapper {display:block;}
+#splashScreen {display:none;}
+
+#displayArea {margin:1em 17em 0 14em;}
+
+.toolbar {text-align:right; font-size:.9em;}
+
+.tiddler {padding:1em 1em 0;}
+
+.missing .viewer,.missing .title {font-style:italic;}
+
+.title {font-size:1.6em; font-weight:bold;}
+
+.missing .subtitle {display:none;}
+.subtitle {font-size:1.1em;}
+
+.tiddler .button {padding:0.2em 0.4em;}
+
+.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
+.isTag .tagging {display:block;}
+.tagged {margin:0.5em; float:right;}
+.tagging, .tagged {font-size:0.9em; padding:0.25em;}
+.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
+.tagClear {clear:both;}
+
+.footer {font-size:.9em;}
+.footer li {display:inline;}
+
+.annotation {padding:0.5em; margin:0.5em;}
+
+* html .viewer pre {width:99%; padding:0 0 1em 0;}
+.viewer {line-height:1.4em; padding-top:0.5em;}
+.viewer .button {margin:0 0.25em; padding:0 0.25em;}
+.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
+.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}
+
+.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
+.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
+table.listView {font-size:0.85em; margin:0.8em 1.0em;}
+table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}
+
+.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
+.viewer code {font-size:1.2em; line-height:1.4em;}
+
+.editor {font-size:1.1em;}
+.editor input, .editor textarea {display:block; width:100%; font:inherit;}
+.editorFooter {padding:0.25em 0; font-size:.9em;}
+.editorFooter .button {padding-top:0px; padding-bottom:0px;}
+
+.fieldsetFix {border:0; padding:0; margin:1px 0px;}
+
+.sparkline {line-height:1em;}
+.sparktick {outline:0;}
+
+.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
+.zoomer div {padding:1em;}
+
+* html #backstage {width:99%;}
+* html #backstageArea {width:99%;}
+#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em;}
+#backstageToolbar {position:relative;}
+#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em;}
+#backstageButton {display:none; position:absolute; z-index:175; top:0; right:0;}
+#backstageButton a {padding:0.1em 0.4em; margin:0.1em;}
+#backstage {position:relative; width:100%; z-index:50;}
+#backstagePanel {display:none; z-index:100; position:absolute; width:90%; margin-left:3em; padding:1em;}
+.backstagePanelFooter {padding-top:0.2em; float:right;}
+.backstagePanelFooter a {padding:0.2em 0.4em;}
+#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}
+
+.whenBackstage {display:none;}
+.backstageVisible .whenBackstage {display:block;}
+/*}}}*/
+
+
+
+
/***
+StyleSheet for use when a translation requires any css style changes.
+This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
+***/
+/*{{{*/
+body {font-size:0.8em;}
+#sidebarOptions {font-size:1.05em;}
+#sidebarOptions a {font-style:normal;}
+#sidebarOptions .sliderPanel {font-size:0.95em;}
+.subtitle {font-size:0.8em;}
+.viewer table.listView {font-size:0.95em;}
+/*}}}*/
+
+
+
/*{{{*/
+@media print {
+#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none !important;}
+#displayArea {margin: 1em 1em 0em;}
+noscript {display:none;} /* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
+}
+/*}}}*/
+
+
+
<!--{{{-->
+<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
+<div class='headerShadow'>
+<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
+<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
+</div>
+<div class='headerForeground'>
+<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
+<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
+</div>
+</div>
+<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
+<div id='sidebar'>
+<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
+<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
+</div>
+<div id='displayArea'>
+<div id='messageArea'></div>
+<div id='tiddlerDisplay'></div>
+</div>
+<!--}}}-->
+
+
+
<!--{{{-->
+<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
+<div class='title' macro='view title'></div>
+<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
+<div class='tagging' macro='tagging'></div>
+<div class='tagged' macro='tags'></div>
+<div class='viewer' macro='view text wikified'></div>
+<div class='tagClear'></div>
+<!--}}}-->
+
+
+
<!--{{{-->
+<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
+<div class='title' macro='view title'></div>
+<div class='editor' macro='edit title'></div>
+<div macro='annotations'></div>
+<div class='editor' macro='edit text'></div>
+<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser excludeLists'></span></div>
+<!--}}}-->
+
+
+
To get started with this blank [[TiddlyWiki]], you'll need to modify the following tiddlers:
+* [[SiteTitle]] & [[SiteSubtitle]]: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
+* [[MainMenu]]: The menu (usually on the left)
+* [[DefaultTiddlers]]: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
+You'll also need to enter your username for signing your edits: <<option txtUserName>>
+
+
+
These [[InterfaceOptions]] for customising [[TiddlyWiki]] are saved in your browser
+
+Your username for signing your edits. Write it as a [[WikiWord]] (eg [[JoeBloggs]])
+
+<<option txtUserName>>
+<<option chkSaveBackups>> [[SaveBackups]]
+<<option chkAutoSave>> [[AutoSave]]
+<<option chkRegExpSearch>> [[RegExpSearch]]
+<<option chkCaseSensitiveSearch>> [[CaseSensitiveSearch]]
+<<option chkAnimate>> [[EnableAnimations]]
+
+----
+Also see [[AdvancedOptions]]
+
+
+
<<importTiddlers>>
+
+
+ +
+
+
---------------------------------------------------------------------
+FOR ANT USERS
+---------------------------------------------------------------------
+
+
+If you are using Ant directly instead of using waf, these instructions apply to you:
+
+in this document, the example instructions are based on local source repository rooted at c:\root. You are free to locate it to anywhere you'd like to.
+3.1 Setup developer build type
+
+       1) Go to c:\cloud\java\build directory
+
+        2) Copy file build-cloud.properties.template to file build-cloud.properties, then modify some of the parameters to match your local setup. The template properties file should have content as
+
+            debug=true
+            debuglevel=lines,vars,source
+            tomcat.home=$TOMCAT_HOME      --> change to your local Tomcat root directory such as c:/apache-tomcat-6.0.18
+            debug.jvmarg=-Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n
+            deprecation=off
+            build.type=developer
+            target.compat.version=1.5
+            source.compat.version=1.5
+            branding.name=default
+
+        3) Make sure the following Environment variables and Path are set:
+
+set enviroment variables:
+CATALINA_HOME:
+JAVA_HOME:  
+CLOUD_HOME:  
+MYSQL_HOME:
+
+update the path to include
+
+MYSQL_HOME\bin
+
+    4) Clone a full directory tree of C:\cloud\java\build\deploy\production to C:\cloud\java\build\deploy\developer
+
+            You can use Windows Explorer to copy the directory tree over. Please note, during your daily development process, whenever you see updates in C:\cloud\java\build\deploy\production, be sure to sync it into C:\cloud\java\build\deploy\developer.
+3.2 Common build instructions
+
+After you have setup the build type, you are ready to perform build and run Management Server alone locally.
+
+cd java
+python waf configure build install
+
+More at Build system.
+
+Will install the management server and its requisites to the appropriate place (your Tomcat instance on Windows, /usr/local on Linux).  It will also install the agent to /usr/local/cloud/agent (this will change in the future).
+4. Database and Server deployment
+
+After a successful management server build (database deployment scripts use some of the artifacts from build process), you can use database deployment script to deploy and initialize the database. You can find the deployment scripts in C:/cloud/java/build/deploy/db.  deploy-db.sh is used to create, populate your DB instance. Please take a look at content of deploy-db.sh for more details
+
+Before you run the scripts, you should edit C:/cloud/java/build/deploy/developer/db/server-setup-dev.xml to allocate Public and Private IP ranges for your development setup. Ensure that the ranges you pick are unallocated to others.
+
+Customized VM templates to be populated are in C:/cloud/java/build/deploy/developer/db/templates-dev.sql  Edit this file to customize the templates to your needs.
+
+Deploy the DB by running
+
+./deploy-db.sh ../developer/db/server-setup-dev.xml ../developer/db/templates-dev.xml
+4.1. Management Server Deployment
+
+ant build-server
+
+Build Management Server
+
+ant deploy-server
+
+Deploy Management Server software to Tomcat environment
+
+ant debug
+
+Start Management Server in debug mode. The JVM debug options can be found in cloud-build.properties
+
+ant run
+
+Start Management Server in normal mode.
+
+5. Agent deployment
+
+After a successful build process, you should be able to find build artifacts at distribution directory, in this example case, for developer build type, the artifacts locate at c:\cloud\java\dist\developer, particularly, if you have run
+
+ant package-agent build command, you should see the agent software be packaged in a single file named agent.zip under c:\cloud\java\dist\developer, together with the agent deployment script deploy-agent.sh.
+5.1 Agent Type
+
+Agent software can be deployed and configured to serve with different roles at run time. In current implementation, there are 3 types of agent configuration, respectively called as Computing Server, Routing Server and Storage Server.
+
+    * When agent software is configured to run as Computing server, it is responsible to host user VMs. Agent software should be running in Xen Dom0 system on computer server machine.
+
+    * When agent software is configured to run as Routing Server, it is responsible to host routing VMs for user virtual network and console proxy system VMs. Routing server serves as the bridge to outside network, the machine that agent software is running should have at least two network interfaces, one towards outside network, one participates the internal VMOps management network. Like computer server, agent software on routing server should also be running in Xen Dom0 system.
+
+    * When agent software is configured to run as Storage server, it is responsible to provide storage service for all VMs. The storage service is based on ZFS running on a Solaris system, agent software on storage server is therefore running under Solaris (actually a Solaris VM), Dom0 systems on computing server and routing server can access the storage service through iScsi initiator. The storage volume will be eventually mounted on Dom0 system and make available to DomU VMs through our agent software.
+
+5.2 Resource sharing
+
+All developers can share the same set of agent server machines for development, to make this possible, the concept of instance appears in various places
+
+    * VM names. VM names are structual names, it contains a instance section that can identify VMs from different VMOps cloud instances. VMOps cloud instance name is configured in server configuration parameter AgentManager/instance.name
+    * iScsi initiator mount point. For Computing servers and Routing servers, the mount point can distinguish the mounted DomU VM images from different agent deployments. The mount location can be specified in agent.properties file with a name-value pair named mount.parent
+    * iScsi target allocation point. For storage servers, this allocation point can distinguish the storage allocation from different storage agent deployments. The allocation point can be specified in agent.properties file with a name-value pair named parent
+
+5.4 Deploy agent software
+
+Before running the deployment scripts, first copy the build artifacts agent.zip and deploy-agent.sh to your personal development directory on agent server machines. By our current convention, you can create your personal development directory that usually locates at /root/your name. In following example, the agent package and deployment scripts are copied to test0.lab.vmops.com and the deployment script file has been marked as executible.
+
+    On build machine,
+
+        scp agent.zip root@test0:/root/your name
+
+        scp deploy-agent.sh root@test0:/root/your name
+
+    On agent server machine
+
+chmod +x deploy-agent.sh
+5.4.1 Deploy agent on computing server
+
+deploy-agent.sh -d /root/<your name>/agent -h <management server IP> -t computing -m expert   
+5.4.2 Deploy agent on routing server
+
+deploy-agent.sh -d /root/<your name>/agent -h <management server IP> -t routing -m expert   
+5.4.3 Deploy agent on storage server
+
+deploy-agent.sh -d /root/<your name>/agent -h <management server IP> -t storage -m expert   
+5.5 Configure agent
+
+After you have deployed the agent software, you should configure the agent by editing the agent.properties file under /root/<your name>/agent/conf directory on each of the Routing, Computing and Storage servers. Add/Edit following properties. The rest are defaults that get populated by the agent at runtime.
+    workers=3
+    host=<replace with your management server IP>
+    port=8250
+    pod=<replace with your pod id>
+    zone=<replace with your zone id>
+   instance=<your unique instance name>
+   developer=true
+
+Following is a sample agent.properties file for Routing server
+
+   workers=3
+   id=1
+   port=8250
+   pod=RC
+   storage=comstar
+   zone=RC
+   type=routing
+   private.network.nic=xenbr0
+   instance=RC
+   public.network.nic=xenbr1
+   developer=true
+   host=192.168.1.138
+5.5 Running agent
+
+Edit /root/<ryour name>/agent/conf/log4j-cloud.xml to update the location of logs to somewhere under /root/<your name>
+
+Once you have deployed and configured the agent software, you are ready to launch it. Under the agent root directory (in our example, /root/<your name>/agent. there is a scrip file named run.sh, you can use it to launch the agent.
+
+Launch agent in detached background process
+
+nohup ./run.sh & 
+
+Launch agent in interactive mode
+
+./run.sh
+
+Launch agent in debug mode, for example, following command makes JVM listen at TCP port 8787
+
+./run.sh -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n
+
+If agent is launched in debug mode, you may use Eclipse IDE to remotely debug it, please note, when you are sharing agent server machine with others, choose a TCP port that is not in use by someone else.
+
+Please also note that, run.sh also searches for /etc/cloud directory for agent.properties, make sure it uses the correct agent.properties file!
+5.5. Stopping the Agents
+
+the pid of the agent process is in /var/run/agent.<Instance>.pid
+
+To Stop the agent:
+
+kill <pid of agent>
+
+ 
+
+
+
!Fedora / CentOS
+# [[Install the build dependencies|waf installrpmdeps]] with {{{./waf installrpmdeps}}} in the source directory.
+# As a non-root user, run the command {{{./waf rpm}}} in the source directory.
+Once this command is done, the packages will be built in the directory {{{artifacts/rpmbuild}}}.
+!Ubuntu
+# [[Install the build dependencies|waf installdebdeps]] with {{{./waf installdebdeps}}} in the source directory.
+# As a non-root user, run the command {{{./waf deb}}} in the source directory.
+Once this command is done, the packages will be built in the directory {{{artifacts/debbuild}}}.
+
+
+
!Obtain the source for the CloudStack
+If you aren't reading this from a local copy of the source code, see [[Obtaining the source]].
+!Prepare your development environment
+See [[Preparing your development environment]].
+!Configure the build on the builder machine
+As non-root, run the command {{{./waf configure}}}.  See [[waf configure]] to discover configuration options for that command.
+!Build the CloudStack on the builder machine
+As non-root, run the command {{{./waf build}}}.  See [[waf build]] for an explanation.
+!Install the CloudStack on the target systems
+On each machine where you intend to run a CloudStack component:
+# upload the entire source code tree after compilation, //ensuring that the source ends up in the same path as the machine in which you compiled it//,
+## {{{rsync}}} is [[usually very handy|Using rsync to quickly transport the source tree to another machine]] for this
+# in that newly uploaded directory of the target machine, run the command {{{./waf install}}} //as root//.
+Consult [[waf install]] for information on installation.
+
+
+
!Changing the [[configuration|waf configure]] process
+See file {{{wscript_configure}}}.
+!Changing the [[build|waf build]] and [[install|waf install]] processes
+!!Changing / adding / removing JAR targets
+You generally need to add a new {{{compile-xyz}}} target following the model of the existing ones, and add that target to the list of dependencies of the other pertinent targets.  See  [[How waf uses ant]] and the ant build project files within the {{{build/}}} folder.
+
+We have some old ant information that you might find useful: AntInformation.
+!!Other changes
+See file {{{wscript_build}}}.
+!Changing packaging
+!!Fedora / """CentOS""" packaging
+See {{{cloud.spec}}} in the source directory.
+!!Ubuntu packaging
+See the files in the {{{debian/}}} folder.
+
+
+
The Cloud.com CloudStack is an open source software product that enables the deployment, management, and configuration of multi-tier and multi-tenant infrastructure cloud services by enterprises and service providers.
+
+
+
Not done yet!
+
+
+
Not done yet!
+
+
+
[[Welcome]]
+
+
+
#[[Source layout guide]]
+
+
+
Not done yet!
+
+
+
Start here if you want to learn the essentials to extend, modify and enhance the CloudStack.  This assumes that you've already familiarized yourself with CloudStack concepts, installation and configuration using the [[Getting started|Welcome]] instructions.
+* [[Obtain the source|Obtaining the source]]
+* [[Prepare your environment|Preparing your development environment]]
+* [[Get acquainted with the development lifecycle|Your development lifecycle]]
+* [[Familiarize yourself with our development conventions|Development conventions]]
+Extra developer information:
+* [[What is this waf thing?|waf]]
+* [[How to change the build, install and packaging processes|Changing the build, install and packaging processes]]
+* [[How to integrate with Eclipse]]
+* [[Starting over]]
+* [[Making a source release|waf dist]]
+
+
+
+
!Importing the projects in the source
+#Open Eclipse
+#Select //Import projects//
+#Point it to the source directory and import all the projects within it
+!Management Server execution
+To run the Management Server from Eclipse, set up an External Tool of the Program variety.  Put the path to the {{{waf}}} binary in the Location of the window, and the source directory as Working Directory.  Then specify {{{install --preserve-config run}}} as arguments.  You can now use the Run button in Eclipse to execute the Management Server directly from Eclipse.  You can replace run with debug if you want to run the Management Server with the Debugging Proxy turned on.
+!Agent or Console Proxy execution
+To run the Agent or Console Proxy from Eclipse, set up an External Tool of the Program variety just like in the Management Server case.  In there, however, specify {{{install --preserve-config run_agent}}} or  {{{install --preserve-config run_console_proxy}}} as arguments instead. Remember that you need to [[set sudo up|Setting sudo up for passwordless root]] to not ask you for a password and not require a TTY, otherwise sudo -- implicitly called by [[waf run_agent]] or [[waf run_console_proxy]] -- will refuse to work.
+
+
+
+
+
By now, you have probably noticed that we do, indeed, ship ant build files in the CloudStack.  During the build process, waf calls ant directly to build the Java portions of our stack, and it uses the resulting JAR files to perform the installation.
+
+Any ant target added to the ant project files will automatically be detected -- if it is named {{{compile-xyz}}} waf will know it builds a JAR file and knows to use / install that JAR file.  In general, this means you only need to add the JAR file to the appropriate  packaging manifests ({{{cloud.spec}}} and {{{debian/{name-of-package}.install}}}).
+
+The reason we do this rather than use the native waf capabilities for building Java projects is simple: by using ant, we can leverage the support built-in for ant in [[Eclipse|How to integrate with Eclipse]] and many other """IDEs""".  Another reason to do this is because Java developers are familiar with ant, so adding a new JAR file or modifying what gets built into the existing JAR files is facilitated for Java developers.
+
+
+
The CloudStack build system installs files on a variety of paths, each
+one of which is selectable when building from source.
+* {{{$PREFIX}}}:
+** the default prefix where the entire stack is installed
+** defaults to /usr/local on source builds
+** defaults to /usr on package builds
+* {{{$SYSCONFDIR/cloud}}}:
+** the prefix for CloudStack configuration files
+** defaults to $PREFIX/etc/cloud on source builds
+** defaults to /etc/cloud on package builds
+* {{{$SYSCONFDIR/init.d}}}:
+** the prefix for CloudStack initscripts
+** defaults to $PREFIX/etc/init.d on source builds
+** defaults to /etc/init.d on package builds
+* {{{$BINDIR}}}:
+** the CloudStack installs programs there
+** defaults to $PREFIX/bin on source builds
+** defaults to /usr/bin on package builds
+* {{{$LIBEXECDIR}}}:
+** the CloudStack installs service runners there
+** defaults to $PREFIX/libexec on source builds
+** defaults to /usr/libexec on package builds (/usr/bin on Ubuntu)
+
+
+
These instructions cover the installation of the entire CloudStack.  If you want to install only specific components, be sure to modify the example commands given here sensibly, so that only the desired packages get installed
+!Fedora / CentOS
+After building packages, they will be available in the directory {{{artifacts/rpmbuild}}}.  You can install them by executing {{{rpm -ivh artifacts/rpmbuild/RPMS/*/*.rpm}}} from the source directory, as root.
+!Ubuntu
+After building packages, they will be available in {{{artifacts/debbuild}}}.  A {{{sudo dpkg -i artifacts/debbuild/*.deb}}} should suffice to install them.
+
+
+
You have three options.  Choose one:
+# [[Installing the CloudStack from the official package repositories]].<br>This is the recommended (and quickest) way to run a stable release of your CloudStack cloud.  The advantages of using this method are that these packages has been tested by (awesome) Cloud.com engineers, you can easily [[update to new CloudStack releases|Updating the CloudStack software]] later on, dependencies are taken care of automatically for you, and you can verify the integrity of the installed files using your system's package manager.
+# [[Building distribution packages]] from the source, then [[installing them|Installing distribution packages built by you]].<br>This is the recommended way to run your CloudStack cloud from unstable sources.  The advantages of using this method are that dependencies are taken care of automatically for you, and you can verify the integrity of the installed files using your system's package manager.
+# [[Building from the source and installing directly from there]].<br>This option is suitable for you if you want to develop the CloudStack or you have an unsupported distribution.  It is also the recommended way to run your CloudStack cloud if you intend to customize or reconfigure the source, if you intend to port the CloudStack to another distribution, or if you intend to run the CloudStack on a distribution for which packages are not built by us.
+
+
+
!Fedora
+!!Add the CloudStack repository to your operating system
+{{{
+cd /etc/yum.repos.d/
+wget http://download.cloud.com/foss/fedora/cloud.repo
+}}}
+!!Install the component you want
+* Management Server: {{{yum install cloud-client}}}
+* Agent: {{{yum install cloud-agent}}}
+* Console Proxy: {{{yum install cloud-console-proxy}}}
+!CentOS
+!!Add the CloudStack repository to your operating system
+{{{
+cd /etc/yum.repos.d/
+wget http://download.cloud.com/foss/centos/cloud.repo
+}}}
+!!Install the component you want
+* Management Server: {{{yum install cloud-client}}}
+* Agent: {{{yum install cloud-agent}}}
+* Console Proxy: {{{yum install cloud-console-proxy}}}
+!Ubuntu
+!!Add the CloudStack repository to your operating system
+Add the following line to {{{/etc/apt/sources.list}}}:
+{{{
+deb http://download.cloud.com/apt/ubuntu/stable/oss ./
+}}}
+!!Install the component you want
+* Management Server: {{{aptitude install cloud-client}}}
+* Agent: {{{aptitude install cloud-agent}}}
+* Console Proxy: {{{aptitude install cloud-console-proxy}}}
+
+
+
 * 
+
+
+
Copyright:
+
+    <Copyright (C) 2009-2010 Cloud.com.>
+
+License:
+
+    This program is dual-licensed.
+    
+    For the free software portions: you can redistribute it and/or modify   it under the terms of the GNU General Public License as published by     the Free Software Foundation, either version 3 of the License, or     (at your option) any later version.  These portions -- clearly marked     throughout the program sources -- are also distributed under the    Cloud.com Software License 1.1.
+    
+    For the proprietary portions: these portions are made available to you    on the terms of the Cloud.com Software License 1.1.
+
+    This package is distributed in the hope that it will be useful,    but WITHOUT ANY WARRANTY; without even the implied warranty of    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+    You should have received a copy of the GNU General Public License    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+
[[Welcome]]
+[[Using this wiki]]
+
+[[Getting started|Welcome]]
+[[Developer info|Hacking on the CloudStack]]
+
+[[Cloud.com|http://cloud.com/]]
+[[Our community|http://cloud.com/community]]
+[[Forums|http://cloud.com/community/forum]]
+[[Report a bug|http://bugs.cloud.com/enter_bug.cgi]]
+
+
+
If you are reading this file directly from a local copy of the source code in your machine, hmmm, well, you can skip this.
+
+Otherwise, there are two options to obtain the source code for the CloudStack:
+!Getting a stable release
+Download the latest tarball from the [[Cloud.com downloads section|http://cloud.com/community/downloads]].
+!Getting the latest, bleeding-edge code
+Use [[Git]] to clone the following URL:
+{{{
+git clone http://git.cloud.com/cloudstack-oss
+}}}
+This will create a folder called {{{cloudstack-oss}}} in your current folder.
+!Browsing the source code online
+You can browse the CloudStack source code through [[our CGit Web interface|http://git.cloud.com/cloudstack-oss]].
+
+
+
!Install the build dependencies on the machine where you will compile the CloudStack
+!!Fedora / CentOS
+The command [[waf installrpmdeps]] issued from the source tree gets it done.
+!!Ubuntu
+The command [[waf installdebdeps]] issues from the source tree gets it done.
+!!Other distributions
+See [[CloudStack build dependencies]]
+!Install the run-time dependencies on the machines where you will run the CloudStack
+See [[CloudStack run-time dependencies]].
+
+
+
Every time you run {{{./waf install}}} to deploy changed code, waf will install configuration files once again.  This can be a nuisance if you are developing the stack.
+
+There are, however, two ways to get around this.
+!Append {{{--preserve-config}}} to executions of {{{./waf install}}}
+{{{./waf install}}} has an option {{{--preserve-config}}}.  If you pass       this option when installing, configuration files are never        overwritten.
+This option is useful when you have modified source files and          you need to deploy them on a system that already has the          CloudStack installed and configured, but you do //not// want to          overwrite the existing configuration of the CloudStack.
+          
+If, however, you have reconfigured and rebuilt the source          since the last time you did ./waf install, then you are          advised to replace the configuration files and set the          components up again, because some configuration files          in the source use identifiers that may have changed during          the last {{{./waf configure}}}.  So, if this is your case, check      out the next technique.
+!Override configuration files in the source tree          
+Every configuration file can be overridden in the source          without touching the original.
+# Look for the specific config file {{{X}}} (or {{{X.in}}}) in the source, then
+# create an {{{override/}}} folder in the folder that contains {{{X}}}, then
+# place a file named {{{X}}} (or {{{X.in}}}) inside {{{override/}}}, then
+# put the desired contents inside {{{X}}} (or {{{X.in}}})
+Now, every time you run {{{./waf install}}}, the file that will be      installed is {{{path/to/override/X.in}}}, instead of {{{path/to/X.in}}}.
+
+This option is useful if you are developing the CloudStack          and constantly reinstalling it.  It guarantees that every          time you install the CloudStack, the installation will have           the correct configuration straight from your source tree and will be ready to run.
+
+
+
It is not technically possible to run the CloudStack components directly from the source tree.  That, however, is fine -- each component can be run independently from the install directory.  Here are instructions to do that, [[after you've set the desired component up|Setting the CloudStack components up]].
+!Management Server
+# Execute {{{./waf run}}} as your current user [[(more info)|waf run]].<br>Alternatively, you can use {{{./waf debug}}} and this will [[run with debugging enabled|waf debug]].
+!Agent (Linux-only):
+# Execute {{{./waf run_agent}}} [[(more info)|waf run_agent]]<br>This will implicitly  launch {{{sudo}}} and require your root password unless you have  set {{{sudo}}} up [[not to ask for it|Setting sudo up for passwordless root]] (advisable).
+!Console Proxy (Linux-only):
+# Execute {{{./waf run_console_proxy}}} [[(more info)|waf run_console_proxy]]<br>This will implicitly  launch {{{sudo}}} and require your root password unless you have  set {{{sudo}}} up [[not to ask for it|Setting sudo up for passwordless root]] (advisable).
+
+
+
When building a source distribution ([[waf dist]]), or distribution packages ([[waf deb]] / [[waf rpm]]), waf will automatically detect the relevant source code control information if the git command is present on the machine where waf is run, and it will write the information to a file called {{{sccs-info}}} inside the source tarball.  Packages and [[waf install]] will install this file into {{{$DOCDIR/sccs-info}}.
+
+If this source code control information cannot be calculated, then the old {{{sccs-info}}} file is preserved -- if it existed -- across [[dist|waf dist]] runs if it exists, and if it did not exist before, the fact that the source could not be properly tracked down to a repository is noted in the file.
+
+
+
+
#Comment option {{{Defaults requiretty}}} in {{{/etc/sudoers}}}
+# Add a line {{{yourusername:    ALL=(ALL)      NOPASSWD: ALL}}} with your username instead of {{{yourusername}}}
+''Warning'': this can be ''insecure'' as it leaves your computer open to root access to whomever touches your console or logs in as your user.
+
+
+
Agent setup requires some system configuration changes, but is fully automated for your benefit.  To set up your Agent, just run {{{$BINDIR/cloud-setup-agent}}}.  It will ask you some questions about the zone and pod it will run on, and then set it up to start on boot.
+
+The configuration files for the Agent live in {{{agent/conf}}} and get deployed to {{{$SYSCONFDIR/cloud/agent}}}.
+
+
+
!Management Server
+Set up the management server database:
+# either run {{{./waf deploydb_kvm}}} for a [[quick development-type database setup|waf deploydb]], or
+# run {{{$BINDIR/cloud-setup-databases}}} to [[set up the Management Server database in production mode|Setting the Management Server up]]
+!Agent (Linux-only)
+[[Set the Agent up|Setting the Agent up]] with {{{$BINDIR/cloud-setup-agent}}}.
+!Console Proxy (Linux-only):
+[[Set the Console Proxy up|Setting the Console Proxy up]] with {{{$BINDIR/cloud-setup-console-proxy}}}.
+
+
+
Please consult the [[Installation Guide|http://cloud.com/community/cloudstack-21-community-edition-installation-guide]] to find out how to configure each component of the CloudStack.  Refer to the [[Installation paths]] document for information on where to find the programs, initscripts and configuration files mentioned in the Installation Guide (paths may vary).
+
+
+
+
Console Proxy setup is fully automated for your benefit.  To set up your Console Proxy, just run {{{$BINDIR/cloud-setup-console-proxy}}}.  It will ask you some questions about the zone and pod it will run on, and then set it up to start on boot.
+
+The configuration files for the Console Proxy live in {{{console-proxy/conf.dom0}}} and get deployed to {{{$SYSCONFDIR/cloud/console-proxy}}}.
+
+
+
Management Server setup is fully automated for your benefit.  To set up your Management Server, just run {{{$BINDIR/cloud-setup-databases}}} with the appropriate command-line options (you can invoke {{{cloud-setup-databases}}} without parameters to get documentation on its options).  This will deploy the database and adjust the Management Server configuration files to use that database.
+
+The configuration files for the Management Server live in {{{client/tomcatconf}}} and get deployed to {{{$SYSCONFDIR/cloud/client}}}.
+
+
+
Documentation
+
+
+
[[Cloud.com|http://cloud.com/]] """CloudStack"""
+
+
+
Here, you'll discover how the source is laid out, what happens to the sources, and where they get installed in target systems.  Refer to the [[Installation paths]] and [[Token substitution of configure-time variables]] documents to find out about the tokens and paths used below.
+* {{{deps/*jar}}}
+** installed in {{{@JAVADIR@/}}}
+* {{{thirdparty/*jar}}}
+** installed in {{{@PREMIUMJAVADIR@/}}}
+* {{{scripts/*}}}
+** @token identifiers are substituted
+** auto-installed in {{{@AGENTLIBDIR@/scripts}}}
+* {{{patches/<virttech>/*}}}
+** @token identifiers are substituted
+** tarred up in a file called {{{patch-<virttech>.tgz}}} (the name is only used in the artifacts directory)
+** tarball auto-installed as {{{@AGENTLIBDIR@/scripts/vm/hypervisor/<virttech>/patch.tgz}}}
+* {{{<project>/src/**/*java}}}
+** runs through [[ant|How waf uses ant]]
+** turns into {{{cloud-<project>.jar}}}
+** installed in{{{ @JAVADIR@/}}}
+* {{{<project>/bindir/*}}}
+** if the file ends in .in, @token identifiers are substituted
+** auto-installed in {{{@BINDIR@/}}}
+* {{{<project>/libexec/*}}}
+** if the file ends in .in, @token identifiers are substituted
+** auto-installed in {{{@LIBEXECDIR@/}}}
+* {{{<project>/tomcatconf/*}}}
+** if the file ends in .in, @token identifiers are substituted
+** auto-installed in {{{@MSCONF@/}}}
+* {{{<project>/db/*}}}
+** auto-installed in {{{@SETUPDATADIR@/}}}
+* {{{<project>/<distro>/SOMEDIRECTORY}}}
+** if the file ends in .in, token identifiers are substituted
+** auto-installed only in specific {{{<distro>}}}, in {{{@SOMEDIRECTORY@/}}} (that is, the uppercase directory name is taken to be an identifier and substituted dynamically for the path that it represents).
+* {{{plugins/<project>/src/**/*java}}}
+** runs through ant
+** turns into {{{cloud-<project>.jar}}}
+** installed in {{{@PLUGINJAVADIR@/}}}
+** will automatically appear in the management server classpath at runtime
+* {{{vendor/<vendor>/tomcatconf/*}}}
+** if the file ends in .in, @token identifiers from ./waf showconfig are substituted
+** auto-installed in {{{@MSCONF@/vendor/<vendor>}}}
+** gets listed first in the management server classpath, so any files there will override files in {{{@MSCONF@}}}
+With your help, by being disciplined -- e.g. thinking about where to add a file, avoiding special cases -- and your suggestions (new classes of files?  we can install them!), we can make this source standards guide grow, with the ultimate goal of making it easier to just add files and have them magically be installed in the right places.
+
+
+
You shouldn't have to.  But:
+# To clean build products: [[waf clean]].
+# To clean the entire artifacts directory and configure-time variables: [[waf distclean]].
+# To uninstall: refer to the source section of [[Uninstalling the CloudStack]].
+
+
+
The prerelease mechanism ({{{--prerelease=BRANCHNAME}}} argument to [[./waf deb|waf deb]] or [[./waf rpm|waf rpm]]) allows developers and builders to build packages with pre-release Release tags.  The Release tags are constructed in such a way that both the build number and the branch name is included, so developers can push these packages to repositories and upgrade them using yum or aptitude without having to delete packages manually and install packages manually every time a new build is done.  Any package built with the prerelease mechanism gets a standard X.Y.Z version number -- and, due to the way that the prerelease Release tags are concocted, always upgrades any older prerelease package already present on any system.  The prerelease mechanism must never be used to create packages that are intended to be released as stable software to the general public.
+
+Relevant distribution documentation:
+*   http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
+*   http://fedoraproject.org/wiki/PackageNamingGuidelines#Pre-Release_packages
+!How the [[build number|waf build]] is treated when the prerelease mechanism is enabled
+The build number gets appended to the Release tag of the package.
+
+
+
In short:
+#Files with an extension of {{{.in}}} and files in the {{conf}}, {{tomcatconf}}, {{bindir}}, {{libexec}} and similar directories within the source have tokens {{{@SOMETOKEN@}}} automatically substituted for the corresponding [[configure variable|waf configure]].
+#Files in the {{{scripts/}}} folder have the token {{@VERSION@}} replaced by the version of the CloudStack upon installation.
+#There is a more detailed layout of how tokens are used in the document [[Source layout guide]].
+
+
+
!For installations done using distribution packages
+If you installed the CloudStack using distribution packages, use your operating system package manager to remove the CloudStack packages.  Examples follow:
+!!Fedora / CentOS
+{{{
+rpm -qa | grep ^cloud- | xargs rpm -e
+}}}
+will erase the CloudStack packages, but will not erase any modified configuration files, cache files or log files.  If you want to remove them:
+{{{
+rm -rf /var/log/cloud /etc/cloud /var/cache/cloud
+}}}
+!!Ubuntu
+{{{
+aptitude purge '~ncloud'
+}}}
+will remove all cloud packages and purge configuration files.
+{{{
+aptitude remove
+}}}
+will remove packages and leave configuration files.
+!For installations done from compiled source
+{{{
+./waf uninstall
+}}}
+issued from the source directory as root, will go through every single file that the install process installed, [[and remove it|waf uninstall]].
+
+
+
!If you installed the CloudStack using [[the official package repositories|Installing the CloudStack from the official package repositories]]
+!!Fedora
+{{{yum update}}}.
+!!CentOS
+{{{yum update}}}.
+!!Ubuntu
+{{{
+aptitude update
+aptitude safe-upgrade
+}}}
+!If you installed from the source, or packages built from the source
+#Download the latest CloudStack release
+#Follow the same procedure you used to install the CloudStack the first time
+
+
+
You can use {{{rsync}}} to very quickly deploy a tree of files into another machine.
+
+Let's assume that you have your source tree in {{{/home/joe/cloud-2.3.4}}}.  To take that tree to another machine:
+{{{
+rsync -avzx --delete /home/joe/cloud-2.3.4/ root@othermachine:/home/joe/cloud-2.3.4/
+}}}
+(assuming, of course, the existence of {{{/home/joe}}} in {{{othermachine}}})
+
+
+
This wiki is organized by bite-size topics called //tiddlers//, heavily linked between each other.  Normally, you can browse through each topic -- the topic opens up in a new section of this window -- or you can use the search mechanism at the top of the right sidebar.  The right sidebar also contains a list of all the tiddlers in this document.
+!Yes, you can edit it
+If you have a local copy of this file, you can edit it.  We encourage you to [[send us your edited file through our bug tracker|http://bugs.cloud.com/enter_bug.cgi]] so we can merge your enhancements!
+
+#Enter your username for signing your edits: <<option txtUserName>>.  This will be stored in a cookie on your browser.
+#Set your options to the right of this page.  I personally prefer to keep AutoSave enabled so I don't have to think about saving the file.  If you have not saved your changes to the wiki yet, this wiki will warn you to save when you close it.
+#Be warned that your browser may ask for authorization to save this file on disk.  It's a scary dialog box, but it's perfectly safe -- go ahead.
+#Learn the wiki [[formatting style|http://tiddlywiki.org/wiki/TiddlyWiki_Markup]].
+#Double-click on the text of a tiddler to modify it.  Hit //done// at the top of the tiddler once you're done.
+#Add your tiddlers:
+##To do so, create a link {{{[[Your article name|Hyperlink title]]}}}in an existing tiddler, save it, and then click the italicized link.
+##As a principle: //keep the tiddlers bite-sized and rely heavily on hyperlinking to make them useful to the readers!//  This practice also makes merging the file into the source code that much easier and reduces the opportunity for merge conflicts.
+##Don't repeat yourself!  If you foresee that a particular chunk of text will be reused in different contexts, put it in a //separate// tiddler and link to that tiddler from the appropriate places.
+##The All and More tabs in the right sidebar lets you find tiddler names to link to, and also orphan tiddlers (tiddlers that nobody links to) or missing tiddlers (nonexistent tiddlers that are linked somewhere).
+#Fundamental tiddlers in this wiki:
+##[[SiteTitle]] & [[SiteSubtitle]]: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
+##[[MainMenu]]: The menu (usually on the left)
+##[[DefaultTiddlers]]: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
+
+
+
+
waf supports an intelligent cache mechanism (disabled by default). waf can resurrect compiled code if the source files that produced it have not changed.  waf will use advanced checksums to prevent stale object files from being resurrected.
+
+To activate it, just set an environment variable {{{WAFCACHE}}} to point to a directory (that you must create) where waf will store the result of compiled files.
+
+
+
Hello, and thanks for your interest in the [[Cloud.com|http://cloud.com/]] CloudStackâ„¢!  The Cloud.com CloudStackâ„¢ is Open Source Software that allows organizations to build Infrastructure as a Service (Iaas) clouds.  Working with server, storage, and networking equipment of your choice, the CloudStack provides a turn-key software stack that dramatically simplifies the process of deploying and managing a cloud.
+!Get started with the CloudStack right now
+We've prepared comprehensive instructions to help you.
+# First, review the [[Installation Guide|http://cloud.com/community/cloudstack-21-community-edition-installation-guide]] sections titled //Overview// and //Prerequisites//.
+#Then, [[install the CloudStack components you want|Installing the CloudStack]] on your target machines.
+# After you're done installing, see [[Setting the CloudStack up]] to configure and run it.
+And, in the unlikely case you would want to uninstall the CloudStack, the document [[Uninstalling the CloudStack]] has you covered.
+!Be part of the [[Cloud.com community|http://cloud.com/community/]]!
+We are more than happy to have you ask us questions, hack our source code, and receive your contributions.
+* To get started developing with the CloudStack, please read [[Hacking on the CloudStack]] for more information.
+* If you are reading this from a copy of the source code in your machine, [[you can edit this document too|Using this wiki]], then share your changes with us.
+* Come to  [[our forums|http://cloud.com/community/forum]], [[sign up|http://cloud.com/community/user/register]] as a member, and post there.  We engineers lurk there to answer your questions.
+* If you find bugs, please [[register|http://bugs.cloud.com/createaccount.cgi]] on [[our bug tracker|http://bugs.cloud.com/]] and [[file a report|http://bugs.cloud.com/enter_bug.cgi]].
+* If you have patches or files to send us get in touch with us at [[info@cloud.com|mailto:info@cloud.com]] or file them as attachments in our bug tracker above.
+!Contact us!
+Cloud.com's contact information is:
+>20400 Stevens Creek Blvd
+>Suite 390
+>Cupertino, CA 95014
+>Tel: +1 (888) 384-0962
+!Legal information
+//Unless otherwise specified// by Cloud.com, Inc., or in the sources themselves, [[this software is OSI certified Open Source Software distributed under the GNU General Public License, version 3|License statement]].  OSI Certified is a certification mark of the Open Source Initiative.  The software powering this documentation is """BSD-licensed""" and obtained from [[TiddlyWiki.com|http://tiddlywiki.com/]].
+
+
+
This is the typical lifecycle that you would follow when hacking on a CloudStack component, assuming that your [[development environment has been set up|Preparing your development environment]]:
+# [[Configure|waf configure]] the source code<br>{{{./waf configure --prefix=/home/youruser/cloudstack}}}
+# [[Build|waf build]] and [[install|waf install]] the CloudStack
+## {{{./waf install}}}
+## [[How to perform these tasks from Eclipse|How to integrate with Eclipse]]
+# [[Set the CloudStack component up|Setting the CloudStack components up]]
+# [[Run the CloudStack component|Running a CloudStack component from source]]
+# Hack on the code
+# Build and install the CloudStack again, [[preserving your existing configuration|Preserving the CloudStack configuration across source reinstalls]]<br>{{{./waf install --preserve-config}}}
+#{{{GOTO 4}}}
+
+
+
See [[Setting sudo up for passwordless root]].
+
+
+
[[waf|http://code.google.com/p/waf/]] is a self-contained, advanced build system written by Thomas Nagy, in the spirit of SCons or the GNU autotools suite.
+* To run waf on Linux / Mac: {{{./waf [...commands...]}}}
+* To run waf on Windows:     {{{waf.bat [...commands...]}}}
+{{{./waf --help}}} should be your first discovery point to find out both the configure-time options and the different processes that you can run using waf.
+!Where to learn more about waf
+* The first and foremost reference material: [[the waf book|http://freehackers.org/~tnagy/wafbook/index.html]].
+* http://code.google.com/p/waf/wiki/CodeSnippets
+* http://code.google.com/p/waf/w/list
+* http://code.google.com/p/waf/wiki/FAQ
+!Why does the CloudStack use waf?
+The CloudStack uses waf to build itself.  waf is a relative newcomer to the build system world; it borrows concepts from SCons and other later-generation build systems:
+# waf is very flexible and rich; unlike other build systems, it covers  the entire life cycle, from compilation to installation to  uninstallation.  it also supports [[dist|waf dist]] (create source tarball),  distcheck (check that the source tarball compiles and installs),  autoconf-like checks for dependencies at compilation time,  and more.  With waf, there is no need to maintain fragile shell scripts to do these tasks -- all of these tasks are already built-in and configurable in a descriptive rather than imperative fashion.
+# waf is self-contained.  A single file, distributed with the project,  enables everything to be built, with only a dependency on Python or Jython,  both of which are freely available and shipped in all Linux computers.
+# waf also supports building projects written in multiple languages   (in the case of the CloudStack, we build from C, Java and Python).
+# Since waf is written in Python, the entire library of the Python  language is available to use in the build process.
+!What happens when waf runs?
+When you run waf, this happens behind the scenes:
+# When you run waf for the first time, it unpacks itself to a hidden  directory {{{.waf-1.X.Y.MD5SUM}}}, including the main program and all the Python libraries it provides and needs.
+# Immediately after unpacking itself, waf reads the {{{wscript}}} file  at the root of the source directory.  After parsing this file and loading the functions defined here, it reads {{{wscript_build}} and {{{wscript_configure}} (from which it builds a {{{build()}}} and a {{{configure()}}} function dynamically) 
+# After loading the build scripts as explained above, waf calls  the functions you specified in the command line as commands
+So, for example, {{{./waf configure build install}}} will:
+* call [[configure()|waf configure]] from {{{wscript_configure}}},
+* call [[build()|waf build]] loaded from the contents of {{{wscript_build}}},
+* call [[build()|waf build]] once more but with {{{Options.is_install = True}}}.
+As part of build(), waf invokes ant to build the Java portion of our stack.
+!If you have waf, why are there ant project files in my source tree?
+See [[How waf uses ant]].
+
+
+
{{{
+./waf build
+}}}
+!What does this do?
+This compiles every file that requires compilation or substitution.  The artifacts of this compilation end up, unless otherwise specified, in {{{artifacts/default}}}
+!When / why should I run this?
+You run this command once after you've [[configured the source|waf configure]], and to trigger compilation of any modifications you have made.  However, if you plan on installing your modifications, you can just run [[waf install]] directly -- install implicitly builds.
+!How does this work?
+This runs the contents of {{{wscript_build}}}, which takes care of discovering and describing what needs to be built, which build products / sources need to be installed, and where they should be installed.  In detail:
+# It compiles source code to object code, calling the C compiler, Python compiler, and [[invoking several ant targets|How waf uses ant]].
+# It [[substitutes configure-time variables|Token substitution of configure-time variables]] in the files that require token substitution.
+# It creates packages for those components that require packaging.
+# It creates the [[SCCS info]] file that you can use to track the provenance of an installation.
+!Viewing a progress bar when building
+Append the {{{--progress}}} option to {{{./waf build}}}.
+!Accelerating builds
+See [[WAFCACHE]].
+
+The commands [[waf deb]] and [[waf rpm]] take advantage of the waf cache.
+!Debugging the build process
+#Almost everything that gets built has a target name. [[waf list_targets]] will list the target names.
+#{{{./waf build -vvvvv}}} will give you //a lot of debugging information// on what waf is doing.
+!Specifying a build number
+Normally, the build number is auto-detected from the [[source code control system|SCCS info]].  You can override it by passing the parameter {{{--build-number}}} when building.  The build number is used internally in the JAR files to construct the {{{Implementation-Version}}} property of the metafile manifest included in the JAR file.
+
+The commands [[waf deb]] and [[waf rpm]] also support the {{{--build-number}}} build-time option.
+
+
+
{{{
+./waf clean
+}}}
+Makes an inventory of all build products in {{{artifacts/default}}}, and removes them.
+
+Contrast to [[waf distclean]].
+
+
+
{{{
+./waf configure --prefix=/directory/that/you/have/write/permission/to
+}}}
+!What does this do?
+This runs the file {{{wscript_configure}}}, which takes care of setting the  variables and options that waf will use for compilation and installation, including the installation directory|Installation paths {{{PREFIX}}} and many other installation paths.  Some of these variables refer to the [[paths where waf will install|Installation paths]] different types of files; some other variables refer to defaults or values that will get [[compiled / substituted in the object code|Token substitution of configure-time variables]].  Some of these settings are //auto-detected// based on the platform you're compiling the code on.
+!When / why should I run this?
+You run this command //once//, in preparation to building the stack, or every time you need to change a configure-time variable.  Once you find an acceptable set of configure-time variables, you should not need to run {{{configure}}} again.
+!What happens if I don't run it?
+For convenience reasons, if you forget to configure the source, waf will autoconfigure itself and select some sensible default configuration options.  By default, {{{PREFIX}}} is {{{/usr/local}}}, but you can set it e.g. to  {{{/home/youruser/cloudstack}}} if you plan to do a non-root install.  Be ware that you can later install the stack as a regular user, but most components need to //run// as root.
+!What variables / options exist for configure?
+In general: refer to the output of {{{./waf configure --help}}}.
+
+Specific, useful options:
+* {{{--no-dep-check}}}: will skip dependency checks for java packages needed to compile (saves 20 seconds when redoing the configure)
+* {{{--with-db-user}}}, {{{--with-db-pw}}}, {{{--with-db-host}}}: informs the build system of the """MySQL""" configuration needed to be able to run [[waf deploydb]]
+After configuration, the configure variables will be available inside the file {{{artifacts/c4che/build.default.py}}} and you will be able to list them with [[waf showconfig]].
+
+
+
{{{
+./waf deb
+}}}
+[[builds Debian packages|Building distribution packages]] of the CloudStack.
+
+This command supports [[the prerelease mechanism|The prerelease mechanism]] and [[custom build numbers|waf build]].  It also supports [[accelerated builds|WAFCACHE]], so if you've already compiled your source tree once with the waf cache enabled, creating packages will be orders of magnitude faster.
+
+
+
See [[waf run]].
+
+
+
{{{
+./waf deploydb_kvm
+}}}
+!What does this do?
+This command runs the function {{{deploydb}}} in {{{wscript}}}, with a paramenter {{{"kvm"}}}.  The function uses the MySQL client to recreate / deploy the database schema and default initialized data for the Cloud.com Management Server.  The credentials and database host this command will use are the ones specified at [[configure time|waf configure]] with the options {{{--with-db-user}}}, {{{--with-db-pw}} and {{{--with-db-host}}}.
+
+''Warning'': This function will //destroy// any existing Cloud.com databases on the target host.
+
+
+
{{{
+./waf dist
+}}}
+Creates a source distribution (bzip2-compressed tarball) of the CloudStack in the source directory.  [[SCCS info]] is automatically determined by {{{dist}}} and included in the tarball.
+
+
+
{{{
+./waf distclean
+}}}
+Completely nukes the {{{artifacts}}} directory, thereby eliminating all build products and [[waf configuration|waf configure]].
+
+Contrast to [[waf clean]].
+
+
+
{{{
+./waf install
+}}}
+!When / why should I run this?
+You run this command when you want to install the CloudStack to the directories specified in the [[configuration|waf configure]].
+
+''Warning'': each time you do {{{./waf install}}}, the configuration files  in the installation directory are ''overwritten''.  Consult the document [[Preserving the CloudStack configuration across source reinstalls]] for techniques to prevent this.
+
+If you are going to install for production, //you should run this  process as root//; that guarantees that the proper permissions and file ownerships are set on certain secure files.   If, conversely, you only want to install the    stack as your own user and in a directory that you have write   permission, it's fine to run {{{./waf install}}} as your own user.
+!What does this do?
+This runs the contents of {{{wscript_build}}}, with an option variable  {{{Options.is_install = True}}}.  When this variable is set, waf will, in addition to compiling whatever needs compilation, install the files described in {{{wscript_build}}}.
+!{{{--destdir}}}
+When installing, you may specify the {{{--destdir=/example/dir}}} option.  This will cause the installation of files to be performed relative to the {{{/example/dir}}} specified as argument to the option.  By way of example, if waf would install the file {{{/usr/bin/cloud-setup-databases}}}, the file would actually be installed in {{{/example/dir/usr/bin/cloud-setup-databases}}}; and so on for all installed files.
+
+This option is implicitly used in the packaging commands [[waf deb]] and [[waf rpm]].
+
+
+
+
+
{{{
+./waf installdebdeps
+}}}
+Parses the build dependency list for DEB packaging and uses aptitude (through [[sudo]]) to install them on your system.
+{{{
+./waf viewdebdeps
+}}}
+Shows a [[list of dependencies|waf viewdebdeps]].
+
+
+
{{{
+./waf installrpmdeps
+}}}
+Parses the build dependency list for RPM packaging and uses yum (through [[sudo]]) to install them on your system.
+{{{
+./waf viewrpmdeps
+}}}
+Shows a [[list of dependencies|waf viewrpmdeps]].
+
+
+
{{{
+./waf list_targets
+}}}
+prints out a list of all known build targets.
+
+You can then run
+{{{
+./waf build --targets=targetname
+}}}
+to build only that specific {{{targetname}}}.
+
+
+
{{{
+./waf rpm
+}}}
+[[builds Fedora or CentOS packages|Building distribution packages]] of the CloudStack.
+
+This command supports [[the prerelease mechanism|The prerelease mechanism]] and [[custom build numbers|waf build]].  It also supports [[accelerated builds|WAFCACHE]], so if you've already compiled your source tree once with the waf cache enabled, creating packages will be orders of magnitude faster.
+
+
+
{{{
+./waf run
+}}}
+!What does this do?
+This command starts the Management Server.
+
+More specifically, what it does is create an environment for the Apache Tomcat server, pre-configured during {{{./waf install}}}, and then start it in the foreground, to run the Management Server servlets and components.  When the Tomcat server starts, the management server is running and you can visit it by opening http://localhost:8080/
+
+Once it's running, you can log on as administrator with the user combination {{{admin}}} / {{{password}}}
+!Run with debugging enabled
+If you are developing the CloudStack, it is useful to be able to latch onto the Management Server process with your debugger.  To do that:
+{{{
+./waf debug
+}}}
+If you prefer the process to be stopped during startup and to have it continue only when your debugger has attached itself:
+{{{
+./waf debug --debug-suspend
+}}}
+
+
+
{{{
+./waf run_agent
+}}}
+Runs the Agent as root by using {{{sudo}}} to invoke it.
+
+
+
{{{
+./waf run_console_proxy
+}}}
+Runs the Console Proxy as root by using {{{sudo}}} to invoke it.
+
+
+
{{{
+./waf showconfig
+}}}
+Displays a summary of the values for each configure-time variable.
+
+
+
{{{
+./waf uninstall
+}}}
+Makes a listing of all the files and directories that would be installed, then removes them from the installation directory.
+
+This command supports the {{{--destdir}}} option as documented in [[waf install]].
+
+
+
See [[waf installdebdeps]].
+
+
+
See [[waf installrpmdeps]].
+
+
+ + + + + + + + + + + + + diff --git a/agent/.classpath b/agent/.classpath new file mode 100644 index 00000000000..662a6fa7415 --- /dev/null +++ b/agent/.classpath @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/agent/.project b/agent/.project new file mode 100644 index 00000000000..8b1ee7d35a6 --- /dev/null +++ b/agent/.project @@ -0,0 +1,17 @@ + + + agent + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/agent/bindir/cloud-setup-agent.in b/agent/bindir/cloud-setup-agent.in new file mode 100755 index 00000000000..c9aeea6d146 --- /dev/null +++ b/agent/bindir/cloud-setup-agent.in @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys, os, subprocess, errno, re, traceback + +# ---- This snippet of code adds the sources path and the waf configured PYTHONDIR to the Python path ---- +# ---- We do this so cloud_utils can be looked up in the following order: +# ---- 1) Sources directory +# ---- 2) waf configured PYTHONDIR +# ---- 3) System Python path +for pythonpath in ( + "@PYTHONDIR@", + os.path.join(os.path.dirname(__file__),os.path.pardir,os.path.pardir,"python","lib"), + ): + if os.path.isdir(pythonpath): sys.path.insert(0,pythonpath) +# ---- End snippet of code ---- +import cloud_utils +from cloud_utils import stderr,CheckFailed,TaskFailed,backup_etc,restore_etc +from cloud_utils import setup_agent_config,stop_service,enable_service +from cloud_utils import exit as bail +from cloud_utils import all, any + + +#--------------- procedure starts here ------------ + +# FÃXME for backup and restore: collect service state for all services so we can restore the system's runtime state back to what it was before +# possible exit states: +# a. system configuration needs administrator attention +# b. automatic reconfiguration failed +# c. process interrupted +# d. everything was configured properly (exit status 0) + +brname = "@PACKAGE@br0" +servicename = "@PACKAGE@-agent" +configfile = "@AGENTSYSCONFDIR@/agent.properties" +backupdir = "@SHAREDSTATEDIR@/@AGENTPATH@/etcbackup" + +#=================== the magic happens here ==================== + +stderr("Welcome to the Cloud Agent setup") +stderr("") + +try: + + # pre-flight checks for things that the administrator must fix + do_check_kvm = not ( "--no-kvm" in sys.argv[1:] ) + try: + for f,n in cloud_utils.preflight_checks( + do_check_kvm=do_check_kvm + ): + stderr(n) + f() + except CheckFailed,e: + stderr(str(e)) + bail(cloud_utils.E_NEEDSMANUALINTERVENTION, + "Cloud Agent setup cannot continue until these issues have been addressed") + + # system configuration tasks that our Cloud Agent setup performs + + try: + tasks = cloud_utils.config_tasks(brname) + if all( [ t.done() for t in tasks ] ): + + stderr("All configuration tasks have been performed already") + + else: + + backup_etc(backupdir) + try: + # run all tasks that have not been done + for t in [ n for n in tasks if not n.done() ]: + t.run() + except: + # oops, something wrong, restore system to earlier state and re-raise + stderr("A fatal issue has been detected -- restoring system configuration.\nPlease be patient; *do not* interrupt this process.") + restore_etc(backupdir) + for t in [ n for n in tasks if hasattr(n,"restore_state") ]: + t.restore_state() + raise + + except (TaskFailed,CheckFailed),e: + # some configuration task or post-flight check failed, we exit right away + stderr(str(e)) + bail(cloud_utils.E_SETUPFAILED,"Cloud Agent setup failed") + + setup_agent_config(configfile) + stderr("Enabling and starting the Cloud Agent") + stop_service(servicename) + enable_service(servicename) + stderr("Cloud Agent restarted") + +except KeyboardInterrupt,e: + # user interrupted, we exit right away + bail(cloud_utils.E_INTERRUPTED,"Cloud Agent setup interrupted") +except SystemExit,e: + # process above handled a failure then called bail(), which raises a SystemExit on CentOS + sys.exit(e.code) +except Exception,e: + # at ths point, any exception has been dealt with cleanly by restoring system config from a backup + # we just inform the user that there was a problem + # and bail prematurely + stderr("Cloud Agent setup has experienced an unrecoverable error. Please report the following technical details to Cloud.com.") + traceback.print_exc() + bail(cloud_utils.E_UNHANDLEDEXCEPTION,"Cloud Agent setup ended prematurely") + +stderr("") +stderr("Cloud Agent setup completed successfully") + +# ========================= end program ======================== diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties new file mode 100644 index 00000000000..60222277f33 --- /dev/null +++ b/agent/conf/agent.properties @@ -0,0 +1,30 @@ +# Sample configuration file for VMOPS agent + +#resource= the java class, which agent load to execute +resource=com.cloud.agent.resource.computing.LibvirtComputingResource + +#workers= number of threads running in agent +workers=5 + +#host= The IP address of management server +host=localhost + +#port = The port management server listening on, default is 8250 +port=8250 + +#pod= The pod, which agent belonged to +pod=default + +#zone= The zone, which agent belonged to +zone=default + +#private.network.device= the private nic device +# if this is commented, it is autodetected on service startup +# private.network.device=cloudbr0 + +#public.network.device= the public nic device +# if this is commented, it is autodetected on service startup +# public.network.device=cloudbr0 + +#guid= a GUID to identify the agent + diff --git a/agent/conf/developer.properties.template b/agent/conf/developer.properties.template new file mode 100644 index 00000000000..3e3fa086b91 --- /dev/null +++ b/agent/conf/developer.properties.template @@ -0,0 +1,38 @@ +#instance=AH +#private.macaddr.start=00:16:3e:77:01:01 +#private.ipaddr.start=192.168.166.128 + +#instance=KM +#private.macaddr.start=00:16:3e:77:02:01 +#private.ipaddr.start=192.168.167.128 + +#instance=KY +#private.macaddr.start=00:16:3e:77:03:01 +#private.ipaddr.start=192.168.168.128 + +#instance=WC +#private.macaddr.start=00:16:3e:77:04:01 +#private.ipaddr.start=192.168.169.128 + +#instance=CV +#private.macaddr.start=00:16:3e:77:05:01 +#private.ipaddr.start=192.168.170.128 + +#instance=KS +#private.macaddr.start=00:16:3e:77:06:01 +#private.ipaddr.start=192.168.171.128 + +#instance=ES +#private.macaddr.start=00:16:3e:77:07:01 +#private.ipaddr.start=192.168.172.128 + +#instance=RC +#private.macaddr.start=00:16:3e:77:08:01 +#private.ipaddr.start=192.168.173.128 + +#instance=AX +#private.macaddr.start=00:16:3e:77:09:01 +#private.ipaddr.start=192.168.174.128 + +private.macaddr.start=@private.macaddr.start@ +private.ipaddr.start=@private.ipaddr.start@ diff --git a/agent/conf/environment.properties.in b/agent/conf/environment.properties.in new file mode 100644 index 00000000000..6b0283dbd02 --- /dev/null +++ b/agent/conf/environment.properties.in @@ -0,0 +1,3 @@ +# management server compile-time environment parameters + +paths.pid=@PIDDIR@ diff --git a/agent/conf/log4j-cloud.xml.in b/agent/conf/log4j-cloud.xml.in new file mode 100644 index 00000000000..9cc49282f55 --- /dev/null +++ b/agent/conf/log4j-cloud.xml.in @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/agent/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-agent.in b/agent/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-agent.in new file mode 100755 index 00000000000..e6fc5f8cca3 --- /dev/null +++ b/agent/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-agent.in @@ -0,0 +1,81 @@ +#!/bin/bash + +# chkconfig: 35 99 10 +# description: Cloud Agent + +# WARNING: if this script is changed, then all other initscripts MUST BE changed to match it as well + +. /etc/rc.d/init.d/functions + +whatami=cloud-agent + +# set environment variables + +SHORTNAME="$whatami" +PIDFILE=@PIDDIR@/"$whatami".pid +LOCKFILE=@LOCKDIR@/"$SHORTNAME" +LOGFILE=@AGENTLOG@ +PROGNAME="Cloud Agent" + +unset OPTIONS +[ -r @SYSCONFDIR@/sysconfig/"$SHORTNAME" ] && source @SYSCONFDIR@/sysconfig/"$SHORTNAME" +DAEMONIZE=@BINDIR@/@PACKAGE@-daemonize +PROG=@LIBEXECDIR@/agent-runner + +start() { + echo -n $"Starting $PROGNAME: " + if hostname --fqdn >/dev/null 2>&1 ; then + daemon --check=$SHORTNAME --pidfile=${PIDFILE} "$DAEMONIZE" \ + -n "$SHORTNAME" -p "$PIDFILE" -l "$LOGFILE" "$PROG" $OPTIONS + RETVAL=$? + echo + else + failure + echo + echo The host name does not resolve properly to an IP address. Cannot start "$PROGNAME". > /dev/stderr + RETVAL=9 + fi + [ $RETVAL = 0 ] && touch ${LOCKFILE} + return $RETVAL +} + +stop() { + echo -n $"Stopping $PROGNAME: " + killproc -p ${PIDFILE} $SHORTNAME # -d 10 $SHORTNAME + RETVAL=$? + echo + [ $RETVAL = 0 ] && rm -f ${LOCKFILE} ${PIDFILE} +} + + +# See how we were called. +case "$1" in + start) + start + ;; + stop) + stop + ;; + status) + status -p ${PIDFILE} $SHORTNAME + RETVAL=$? + ;; + restart) + stop + sleep 3 + start + ;; + condrestart) + if status -p ${PIDFILE} $SHORTNAME >&/dev/null; then + stop + sleep 3 + start + fi + ;; + *) + echo $"Usage: $whatami {start|stop|restart|condrestart|status|help}" + RETVAL=3 +esac + +exit $RETVAL + diff --git a/agent/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-agent.in b/agent/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-agent.in new file mode 100755 index 00000000000..e6fc5f8cca3 --- /dev/null +++ b/agent/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-agent.in @@ -0,0 +1,81 @@ +#!/bin/bash + +# chkconfig: 35 99 10 +# description: Cloud Agent + +# WARNING: if this script is changed, then all other initscripts MUST BE changed to match it as well + +. /etc/rc.d/init.d/functions + +whatami=cloud-agent + +# set environment variables + +SHORTNAME="$whatami" +PIDFILE=@PIDDIR@/"$whatami".pid +LOCKFILE=@LOCKDIR@/"$SHORTNAME" +LOGFILE=@AGENTLOG@ +PROGNAME="Cloud Agent" + +unset OPTIONS +[ -r @SYSCONFDIR@/sysconfig/"$SHORTNAME" ] && source @SYSCONFDIR@/sysconfig/"$SHORTNAME" +DAEMONIZE=@BINDIR@/@PACKAGE@-daemonize +PROG=@LIBEXECDIR@/agent-runner + +start() { + echo -n $"Starting $PROGNAME: " + if hostname --fqdn >/dev/null 2>&1 ; then + daemon --check=$SHORTNAME --pidfile=${PIDFILE} "$DAEMONIZE" \ + -n "$SHORTNAME" -p "$PIDFILE" -l "$LOGFILE" "$PROG" $OPTIONS + RETVAL=$? + echo + else + failure + echo + echo The host name does not resolve properly to an IP address. Cannot start "$PROGNAME". > /dev/stderr + RETVAL=9 + fi + [ $RETVAL = 0 ] && touch ${LOCKFILE} + return $RETVAL +} + +stop() { + echo -n $"Stopping $PROGNAME: " + killproc -p ${PIDFILE} $SHORTNAME # -d 10 $SHORTNAME + RETVAL=$? + echo + [ $RETVAL = 0 ] && rm -f ${LOCKFILE} ${PIDFILE} +} + + +# See how we were called. +case "$1" in + start) + start + ;; + stop) + stop + ;; + status) + status -p ${PIDFILE} $SHORTNAME + RETVAL=$? + ;; + restart) + stop + sleep 3 + start + ;; + condrestart) + if status -p ${PIDFILE} $SHORTNAME >&/dev/null; then + stop + sleep 3 + start + fi + ;; + *) + echo $"Usage: $whatami {start|stop|restart|condrestart|status|help}" + RETVAL=3 +esac + +exit $RETVAL + diff --git a/agent/distro/ubuntu/SYSCONFDIR/init.d/cloud-agent.in b/agent/distro/ubuntu/SYSCONFDIR/init.d/cloud-agent.in new file mode 100755 index 00000000000..11e685e99de --- /dev/null +++ b/agent/distro/ubuntu/SYSCONFDIR/init.d/cloud-agent.in @@ -0,0 +1,103 @@ +#!/bin/bash + +# chkconfig: 35 99 10 +# description: Cloud Agent + +# WARNING: if this script is changed, then all other initscripts MUST BE changed to match it as well + +. /lib/lsb/init-functions +. /etc/default/rcS + +whatami=cloud-agent + +# set environment variables + +SHORTNAME="$whatami" +PIDFILE=@PIDDIR@/"$whatami".pid +LOCKFILE=@LOCKDIR@/"$SHORTNAME" +LOGFILE=@AGENTLOG@ +PROGNAME="Cloud Agent" + +unset OPTIONS +[ -r @SYSCONFDIR@/default/"$SHORTNAME" ] && source @SYSCONFDIR@/default/"$SHORTNAME" +DAEMONIZE=@BINDIR@/@PACKAGE@-daemonize +PROG=@LIBEXECDIR@/agent-runner + +start() { + log_daemon_msg $"Starting $PROGNAME" "$SHORTNAME" + if [ -s "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") >/dev/null 2>&1; then + log_progress_msg "apparently already running" + log_end_msg 0 + exit 0 + fi + if hostname --fqdn >/dev/null 2>&1 ; then + true + else + log_failure_msg "The host name does not resolve properly to an IP address. Cannot start $PROGNAME" + log_end_msg 1 + exit 1 + fi + + # Workaround for ubuntu bug:577264, make sure cgconfig/cgred start before libvirt + service cgred stop + service cgconfig stop + service cgconfig start + service cgred start + service libvirt-bin restart + + + if start-stop-daemon --start --quiet \ + --pidfile "$PIDFILE" \ + --exec "$DAEMONIZE" -- -n "$SHORTNAME" -p "$PIDFILE" -l "$LOGFILE" "$PROG" $OPTIONS + RETVAL=$? + then + rc=0 + sleep 1 + if ! kill -0 $(cat "$PIDFILE") >/dev/null 2>&1; then + log_failure_msg "$PROG failed to start" + rc=1 + fi + else + rc=1 + fi + + if [ $rc -eq 0 ]; then + log_end_msg 0 + else + log_end_msg 1 + rm -f "$PIDFILE" + fi +} + +stop() { + echo -n $"Stopping $PROGNAME" "$SHORTNAME" + start-stop-daemon --stop --quiet --oknodo --pidfile "$PIDFILE" + log_end_msg $? + rm -f "$PIDFILE" +} + + +# See how we were called. +case "$1" in + start) + start + ;; + stop) + stop + ;; + status) + status_of_proc -p "$PIDFILE" "$PROG" "$SHORTNAME" + RETVAL=$? + ;; + restart) + stop + sleep 3 + start + ;; + *) + echo $"Usage: $whatami {start|stop|restart|status|help}" + RETVAL=3 +esac + +exit $RETVAL + diff --git a/agent/distro/ubuntu/SYSCONFDIR/init/cloud-cleanup.conf b/agent/distro/ubuntu/SYSCONFDIR/init/cloud-cleanup.conf new file mode 100644 index 00000000000..d06fc9decf0 --- /dev/null +++ b/agent/distro/ubuntu/SYSCONFDIR/init/cloud-cleanup.conf @@ -0,0 +1,15 @@ +description "Stop CloudStack VMs on shutdown" +author "Manuel Amador (Rudd-O) " + +start on stopping libvirt-bin + +task +script + curr_runlevel=`runlevel | tail -c 2` + if [ "$curr_runlevel" = "6" -o "$curr_runlevel" = "0" ] ; then + for a in `virsh list | awk ' /^ +[0-9]+ [vri]-([0-9]+?)-/ { print $2 } '` ; do + echo Destroying CloudStack VM $a + virsh destroy $a + done + fi +end script diff --git a/agent/doc/README-iscsi.txt b/agent/doc/README-iscsi.txt new file mode 100644 index 00000000000..6ad468c6a16 --- /dev/null +++ b/agent/doc/README-iscsi.txt @@ -0,0 +1,230 @@ +0. Contents +=========== +../sbin/vnetd: userspace daemon that runs the vnet +../module/2.6.18/vnet_module.ko: kernel module (alternative to vnetd) +../vnetd.sh: init script for vnet +../vn: helper script to create vnets + +../id_rsa: the private key used to ssh to the routing domain + +createvm.sh: clones a vm image from a given template +mountvm.sh: script to mount a remote (nfs) image directory +runvm.sh: script to run a vm +rundomr.sh: script to run a routing domain (domR) for a given vnet +listvmdisk.sh: lists disks belonging to a vm +createtmplt.sh: installs a template +listvmdisksize.sh: lists actuala and total usage per disk +../ipassoc.sh: associate / de-associate a public ip with an instance +../firewall.sh: add or remove firewall rules +stopvm.sh: stop the vm and remove the entry from xend +../delvm.sh: delete the vm image from zfs +../listclones.sh: list all filesystems that are clones under a parent fs + +1. Install +========== +On the hosts that run the customer vms as well as the domR +a) Copy vn to /usr/sbin on dom0 +b) Copy module/2.6.18/vnet_module.ko to /lib/modules/`uname -r`/kernel +c) Run repos/vmdev/xen/xen-3.3.0/tools/vnet/examples/vnet-insert +Ensure that all iptables rules are flushed from domO before starting any domains +(use iptables -F) +d) Ensure that the ISCSI initiator is installed (yum install iscsi*) + + +2. Creating /deleting a vm image on Solaris ZFS +================ +The template image consists of a filesystem to hold kernel and ramdisk (linux) +or the pygrub file (linux) or nothing (windows). Contained within the template +filesystem (but not visible using 'ls') is the root volume. + +Use the createvm script to clone a template snapshot. For example: +./createvm.sh -t tank/volumes/demo/template/public/os/centos52-x86_64 -d /tank/volumes/demo/template/public/datadisk/ext3-8g -i /tank/demo/vm/chiradeep/i0007 -u /tank/demo/vm/chiradeep + -t: the template fs snapshot + -i: the target clone fs + -u: the user's fs under which the clone will be created. If the user fs does not exist, it will be created. + -d: the disk fs to be cloned under the image dir specified by -i +Once this is created, use the listvmdisk.sh to list the disks: +listvmdisk.sh -i tank/demo/vm/chiradeep/i0007 -r (for the root disk) +listvmdisk.sh -i tank/demo/vm/chiradeep/i0007 -w (for the data disk) +listvmdisk.sh -i tank/demo/vm/chiradeep/i0007 -d (for the data disks) + +This outputs the local target name (zfs name) and the ISCSI target name +separated by a comma: +tank/demo/vm/chiradeep/i0007/datadisk1-ext3-8g,iqn.1986-03.com.sun:02:0b6c18c9-7a13-e7c9-ce78-91af20023bb3 + +The local target name can be used to list total (-t)and actual(-a) disk usage: +./listvmdisksize.sh -d tank/demo/vm/chiradeep/i0007/datadisk1-ext3-8g -t +8589934592 + +Use the delvm.sh script to delete an instance. For example: +./delvm.sh -u tank/demo/vm/chiradeep -i tank/demo/vm/chiradeep/i0007 + -i: the instance fs to delete + -u: the user fs to delete +Either -i or -u or both can be supplied. + +Use the listclones.sh script to list all clones under a parent fs: +./listclones.sh -p tank/demo/vm + +3. Mounting an image +================== +The image directory resides on the NFS server, you can mount it with the +mountvm.sh script. For example: +./mountvm.sh -m -h 192.168.1.248 -t iqn.1986-03.com.sun:02:bf65dcfd-42b5-6e0e-e08e-99ae311b39ba -l /images/chiradeep/i0005 -n centos52 -r tank/demo/vm/chiradeep/i0005 -1 iqn.1986-03.com.sun:02:6d505eee-bf64-6729-e362-bab6c148bbc8 + -h : the nfs/iscsi server host + -l : the local directory + -r : the remote directory + -n : the vm name (the same name used in runvm or rundomr) + -r : the iscsi target name for the root volume (see listvmdisk above) + -w : the iscsi target name for the swap volume (see listvmdisk above) + -1 : the iscsi target name for the datadisk volume (see listvmdisk above) + [-m | -u] : mount or unmount + +4. Routing Domain (domR) +======================= +The routing domain for a customer needs to be started before any other VM in that vnet can start. To start a routing domain, for example: +./rundomr.sh -v 0008 -m 128 -i 192.168.1.33 -g 65.37.141.1 -a aa:00:00:05:00:33 -l "domR-vnet0008" -A 06:01:02:03:04:05 -p 02:01:02:03:04:05 -n 255.255.255.0 -I 65.37.141.33 -N 255.255.255.128 -b eth1 -d "dns1=192.168.1.254 dns2=207.69.188.186 domain=vmops.org" /images/chiradeep/router + -v : the is the 16-bit vnet-id specified in 4 hex characters + -m : the ram size for the domain in megabytes (128 is usually sufficient) + -a : the mac address of the eth0 of the domR + -A : the mac address of the eth1 of the domR + -p : the mac address of the eth2 of the domR + -i : the eth1 ip address in the datacenter LAN (e.g., 192.168.1.33) + -n : the netmask of eth1 + -I : the eth2 ip address in the public LAN (e.g., 65.37.141.33) + -N : the netmask of eth2 (e.g., 65.37.141.128) + -b : the Xen bridge (typ.eth1) that eth2 has to be enslaved to (public LAN) + -g : the default gateway in the public subnet (e.g., 65.37.141.1) + -l : the vm name for the doMR + -d : nameserver information in the format shown in the example +Note: -d option requires template tank/demo/template/public/t100001@12_16_2008 +or later + +5. Starting a vm +================ +The VM files are assumed to exist in a single image directory with the following conventions: + a) The kernel file begins with vmlinuz (e.g. vmlinuz-2.6.18.8-xen) (Linux) + b) The root volume begins with vmi-root (e.g.,vmi-root-centos52-x86_64-pv) + c) The data partition begins with datadisk1 (e.g., datadisk1-ext3-8g) + d) The swap partition contains "swap" (e.g., fedora-swap) (Linux only) + +If booting Linux using pygrub, only the root and data files are needed. An +empty file called 'pygrub' must be placed in the image directory + +To run the vm, see the following example +/runvm.sh -v 0005 -i 10.1.1.56 -m 256 -g 192.168.1.33 -a 02:00:00:05:00:56 -l "centos5-2" -c 11 -n 2 -u 66 /images/chiradeep/i0007 + + -v : the is the 16-bit vnet-id specified in 4 hex characters + -i : this is the host ip address in the 10.x.y.z subnet (cannot be 10.1.1.1) + -m : the ram size for the domain in megabytes + -g : the eth1 ip address of the routing domain + -a : the mac address of the eth0 of the vm + -l : the vm name. This is also the hostname, ensure it is is a legal hostname + -c : the VNC console id + -w : the VNC password. If not specified, defaults to 'password' + -n : the number of VCPUs (eq to number of cores) to allocate (default all) + -u : the percentage of one VCPU to allocate (integer) (default no cap) + : the absolute path of the directory holding the VM files/volumes + +The vncviewer can connect to the eth0 ip of dom0 and the specified vnc console number (e.g., 192.168.1.125:11). +The 'n' and 'u' parameters depends on the physical CPU of the host and the +number of compute units requested. For example, lets say 1 compute unit = 1Ghz +and the physical CPU is a quad-core CPU running at 3.0 Ghz. To request 2 cores +running 1 compute unit each, n = 2 and u= 2 x (1/3)*100 + +6. Associate a public Ip with a domR (source NAT) +=========================================== +The example below shows how to associate the public ip 65.37.141.33 the +routing domain. This has to be run on the dom0 of the host hosting the +routing domain. + +ipassoc.sh -A -r domR-vnet0007 -i 192.168.1.32 -l 65.37.141.33 -a 06:01:02:03:06:05 + -A|-D: create or delete an association + -r: the name (label) of the routing domain + -i: the eth1 ip of the routing domain + -a: the mac address of eth2 in the routing domain (not required for -D) + -l: the public ip to be used for source NAT + +7. Firewall rules +================= +Each instance can have firewall rules associated to allow +some ports through. By default, when created, an instance has all ports and +protocols blocked. In the following example, the 10.1.1.155 instance gets ssh +traffic and icmp pings opened up: +firewall.sh -A -i 192.168.1.133 -P tcp -p 22 -r 10.1.1.155 -l 65.37.141.33 -d +22 +firewall.sh -A -i 192.168.1.133 -P icmp -t echo-request -r 10.1.1.155 -l +65.37.141.33 + -A|-D: add or delete a rule + -i: the eth1 ip of the routing domain + -r: the local eth0 ip of the target instance + -l: the public ip + -P: the protocol (tcp, udp, icmp) + -t: (for icmp) the icmp type + -p: (for tcp and udp) the port (port range in the form of a:b) + -d: (for tcp and udp) the target port (port range in the form of a:b) + +8. Stopping and restarting a VM +=============================== +You can use 'xm reboot vmname' to reboot the VM. +To stop it (and delete it from Xend's internal database), use +stopvm.sh -l +This will not remove the vnet however. +The stopvm script will NOT attempt to umount the root and data disks as well +To explicitly unmount the root disk data disks from the NFS server, run +this on dom0: +mountvm.sh -u -l /images/u00000002/i0003 + -u: (no arguments) + -l: the local directory on the compute server + +9. Vnet cleanup +=============== +When you kill the vnet task, all vnif* interfaces will disappear but the +bridges will linger. +You can use vnetcleanup.sh to clean up the vnet +vnetcleanup.sh -a will clean up all vnets +vnetcleanup.sh -v 0005 will only cleanup vnet0005. + +10. VM Image Cleanup +=================== +On ZFS, run delvm.sh, for example: + ./delvm.sh -u tank/demo/vm/u00000003 -i tank/demo/vm/u00000003/i0001 + -u: the user fs (optional) + -i: the instance fs (optional) + +11. Template installation +========================= +Template installation involves copying the image file of the rootdisk to a +iscsi volume. For example: +createtmplt.sh -t rpool/volumes/demo/template/public/os/ubuntu8 -f +/rpool/volumes/demo/template/public/download/ubuntu8/ubuntu8.0.img -n ubuntu8 -s 12G + -t: the filesystem (created if non-existent) where the volume will be mounted + -f: the absolute path to the file containing the root disk image + -n: the name of the template. The create volume will be vmi-root-$name + -s: the size in gigabytes for the volume + -h: if a hvm image + +12. Mapping iscsi target names to VM names +========================================== +The mapiscsi.sh script maps iscsi names of targets logged in to by the routing +host/compute host: +[root@r-1-1-1 iscsi]# ./mapiscsi.sh +iqn.1986-03.com.sun:02:ef4942ec-9f7e-4d71-e994-bb670867053e r-870-TEST-0186-root +iqn.1986-03.com.sun:02:599f5cc5-2f90-c1c3-9c5e-fef252345e64 r-870-TEST-0186-swap +iqn.1986-03.com.sun:02:0e893b01-fa32-682e-976d-d15781cf1a44 r-872-TEST-0187-root +iqn.1986-03.com.sun:02:21225d22-479c-4a35-dca0-ad56e60aa6f4 r-872-TEST-0187-swap +iqn.1986-03.com.sun:02:55b1a6d4-d202-e565-ffe1-ee63e4a48210 r-875-TEST-0188-root +iqn.1986-03.com.sun:02:4fac467c-7b63-6ffb-c207-aa35ccecfcd5 r-875-TEST-0188-swap + +If no VM name can be found, the second field is blank + +13. OpenVZ patch workarounds +============================ +The openvz patch eliminates kernel oops related to bride reconfiguration. +However this requires an extra tickle to the bridge to make it actually send +packets to member port. The member port needs to be taken down (ifconfig down) +and up (ifconfig up). +This is done in +a) rundomr.sh -- on creation of vnet bridge, the vnif is taken down and up +b) runvm.sh -- ditto +c) /etc/xen/qemu-ifup -- the interface (tapX.0) is taken down and then up +after the interface is added to the bridge. diff --git a/agent/doc/README.txt b/agent/doc/README.txt new file mode 100644 index 00000000000..3eb90d5949e --- /dev/null +++ b/agent/doc/README.txt @@ -0,0 +1,194 @@ +0. Contents +=========== +sbin/vnetd: userspace daemon that runs the vnet +module/2.6.18/vnet_module.ko: kernel module (alternative to vnetd) +vnetd.sh: init script for vnet +vn: helper script to create vnets + +id_rsa: the private key used to ssh to the routing domain + +createvm.sh: clones a vm image from a given template +mountvm.sh: script to mount a remote (nfs) image directory +runvm.sh: script to run a vm +rundomr.sh: script to run a routing domain (domR) for a given vnet +ipassoc.sh: associate / de-associate a public ip with an instance +firewall.sh: add or remove firewall rules +stopvm.sh: stop the vm and remove the entry from xend +delvm.sh: delete the vm image from zfs +loadbalancer.sh: configure the loadbalancer +listclones.sh: list all filesystems that are clones under a parent fs + +1. Install +========== +On the hosts that run the customer vms as well as the domR +a) Copy vn to /usr/sbin on dom0 +Either (vnetd): + 1) Copy sbin/vnetd to /usr/sbin on dom0 + 2) Copy vnetd.sh to /etc/init.d/vnetd on dom0 + 3) run chkconfig vnetd on +OR + 1) Copy module/2.6.18/vnet_module.ko to /lib/modules/`uname -r`/kernel + 2) Run repos/vmdev/xen/xen-3.3.0/tools/vnet/examples/vnet-insert +Ensure that all iptables rules are flushed from domO before starting any domains +(use iptables -F) + + +2. Creating /deleting a vm image on Solaris ZFS +================ +Use the createvm script to clone a template snapshot. For example: +createvm.sh -t tank/template/public/t100001@12_3_2008 -i tank/demo/vm/u00000002/i0001 -u tank/demo/vm/u00000002 -d /tank/demo/template/public/datadisk/ext3-8g + -t: the template fs snapshot + -i: the target clone fs + -u: the user's fs under which the clone will be created. If the user fs does not exist, it will be created. + -d: the disk fs to be cloned under the image dir specified by -i +Once this is created, use the listvmdisk.sh to list the disks: +listvmdisk.sh -i tank/demo/vm/u00000002/i0001 -r (for the root disk) +listvmdisk.sh -i tank/demo/vm/u00000002/i0001 -d (for the data disks) + +Use the delvm.sh script to delete an instance. For example: +./delvm.sh -u tank/demo/vm/u00000003 -i tank/demo/vm/u00000003/i0001 + -i: the instance fs to delete + -u: the user fs to delete +Either -i or -u or both can be supplied. + +Use the listclones.sh script to list all clones under a parent fs: +./listclones.sh -p tank/demo/vm + +3. Mounting an image +================== +If the image directory resides on the NFS server, you can mount it with the +mountvm.sh script. For example: +./mountvm.sh -h sol10-1.lab.vmops.com -l /images/u00000002/i0001 -r +/tank/vm/demo/u00000002/i0001 -m + -h : the nfs server host + -l : the local directory + -r : the remote directory + [-m | -u] : mount or unmount + + +4. Routing Domain (domR) +======================= +The routing domain for a customer needs to be started before any other VM in that vnet can start. To start a routing domain, for example: +./rundomr.sh -v 0008 -m 128 -i 192.168.1.33 -g 65.37.141.1 -a aa:00:00:05:00:33 -l "domR-vnet0008" -A 06:01:02:03:04:05 -p 02:01:02:03:04:05 -n 255.255.255.0 -I 65.37.141.33 -N 255.255.255.128 -b eth1 -d "dns1=192.168.1.254 dns2=207.69.188.186 domain=vmops.org" /images/templates/t100001 + -v : the is the 16-bit vnet-id specified in 4 hex characters + -m : the ram size for the domain in megabytes (128 is usually sufficient) + -a : the mac address of the eth0 of the domR + -A : the mac address of the eth1 of the domR + -p : the mac address of the eth2 of the domR + -i : the eth1 ip address in the datacenter LAN (e.g., 192.168.1.33) + -n : the netmask of eth1 + -I : the eth2 ip address in the public LAN (e.g., 65.37.141.33) + -N : the netmask of eth2 (e.g., 65.37.141.128) + -b : the Xen bridge (typ.eth1) that eth2 has to be enslaved to (public LAN) + -g : the default gateway in the public subnet (e.g., 65.37.141.1) + -l : the vm name for the doMR + -d : nameserver information in the format shown in the example +Note: -d option requires template tank/demo/template/public/t100001@12_16_2008 +or later + +5. Starting a vm +================ +The VM files are assumed to exist in a single image directory with the following conventions: + a) The kernel file begins with vmlinuz (e.g. vmlinuz-2.6.18.8-xen) (Linux) + b) The root filesystem begins with vmi-root (e.g., vmi-root-centos.5-2.64.img) + c) The data partition begins with vmi-data1 (e.g., vmi-data1.img) + d) The swap partition ends with ".swap" (e.g., centos64.swap) (Linux only) + +If booting Linux using pygrub, only the root and data files are needed. An +empty file called 'pygrub' must be placed in the image directory + +To run the vm, see the following example +/runvm.sh -v 0005 -i 10.1.1.122 -m 256 -g 192.168.1.108 -a 02:00:00:05:00:22 -l "centos.5-2.64" -c 11 -n 2 -u 66 /images/u00000002/i0003 + + -v : the is the 16-bit vnet-id specified in 4 hex characters + -i : this is the host ip address in the 10.x.y.z subnet (cannot be 10.1.1.1) + -m : the ram size for the domain in megabytes + -g : the eth1 ip address of the routing domain + -a : the mac address of the eth0 of the vm + -l : the vm name. This is also the hostname, ensure it is is a legal hostname + -c : the VNC console id + -w : the VNC password. If not specified, defaults to 'password' + -n : the number of VCPUs (eq to number of cores) to allocate (default all) + -u : the percentage of one VCPU to allocate (integer) (default no cap) + : the absolute path of the directory holding the VM files + +The vncviewer can connect to the eth0 ip of dom0 and the specified vnc console number (e.g., 192.168.1.125:11). +The 'n' and 'u' parameters depends on the physical CPU of the host and the +number of compute units requested. For example, lets say 1 compute unit = 1Ghz +and the physical CPU is a quad-core CPU running at 3.0 Ghz. To request 2 cores +running 1 compute unit each, n = 2 and u= 2 x (1/3)*100 + +6. Associate a public Ip with a domR (source NAT) +=========================================== +The example below shows how to associate the public ip 65.37.141.33 the +routing domain. This has to be run on the dom0 of the host hosting the +routing domain. + +ipassoc.sh -A -r domR-vnet0007 -i 192.168.1.32 -l 65.37.141.33 -a 06:01:02:03:06:05 + -A|-D: create or delete an association + -r: the name (label) of the routing domain + -i: the eth1 ip of the routing domain + -a: the mac address of eth2 in the routing domain (not required for -D) + -l: the public ip to be used for source NAT + +7. Firewall rules +================= +Each instance can have firewall rules associated to allow +some ports through. By default, when created, an instance has all ports and +protocols blocked. In the following example, the 10.1.1.155 instance gets ssh +traffic and icmp pings opened up: +firewall.sh -A -i 192.168.1.133 -P tcp -p 22 -r 10.1.1.155 -l 65.37.141.33 -d +22 +firewall.sh -A -i 192.168.1.133 -P icmp -t echo-request -r 10.1.1.155 -l +65.37.141.33 + -A|-D: add or delete a rule + -i: the eth1 ip of the routing domain + -r: the local eth0 ip of the target instance + -l: the public ip + -P: the protocol (tcp, udp, icmp) + -t: (for icmp) the icmp type + -p: (for tcp and udp) the port (port range in the form of a:b) + -d: (for tcp and udp) the target port (port range in the form of a:b) + +7.5 Loadbalancer rules +===================== +Loadbalancing is provided by HAProxy running within the routing domain. Because the rules are large and consist of many components, it is expected that the entire HAProxy configuration file is provided to the script. This is copied over to the routing domain and the haproxy process is restarted. +loadbalancer.sh -A -i 192.168.1.35 -l 65.37.141.30 -d 80 -f /tmp/haproxy.cfg +New haproxy instance successfully loaded, stopping previous one. + -A|-D: add or delete a rule + -i: the eth1 ip of the routing domain + -l: the public ip + -d: the target port + -f: the haproxy configuration file + +8. Stopping and restarting a VM +=============================== +You can use 'xm reboot vmname' to reboot the VM. +To stop it (and delete it from Xend's internal database), use +stopvm.sh -l +This will not remove the vnet however. +The stopvm script will attempt to umount the root and data disks as well +To explicitly unmount the root disk data disks from the NFS server, run +this on dom0: +mountvm.sh -u -l /images/u00000002/i0003 + -u: (no arguments) + -l: the local directory on the compute server + +9. Vnet cleanup +=============== +When you kill the vnet task, all vnif* interfaces will disappear but the +bridges will linger. +You can use vnetcleanup.sh to clean up the vnet +vnetcleanup.sh -a will clean up all vnets +vnetcleanup.sh -v 0005 will only cleanup vnet0005. + +10. VM Image Cleanup +=================== +On ZFS, run delvm.sh, for example: + ./delvm.sh -u tank/demo/vm/u00000003 -i tank/demo/vm/u00000003/i0001 + -u: the user fs (optional) + -i: the instance fs (optional) + +10. TODO +======= +5. Automatic install instead of manual steps of (1) diff --git a/agent/libexec/agent-runner.in b/agent/libexec/agent-runner.in new file mode 100755 index 00000000000..8983aeda738 --- /dev/null +++ b/agent/libexec/agent-runner.in @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +#run.sh runs the agent client. + +cd `dirname "$0"` + +SYSTEMJARS="@SYSTEMJARS@" +SCP=$(build-classpath $SYSTEMJARS) ; if [ $? != 0 ] ; then SCP="@SYSTEMCLASSPATH@" ; fi +DCP="@DEPSCLASSPATH@" +ACP="@AGENTCLASSPATH@" +export CLASSPATH=$SCP:$DCP:$ACP:@AGENTSYSCONFDIR@ +for jarfile in "@PREMIUMJAVADIR@"/* ; do + if [ ! -e "$jarfile" ] ; then continue ; fi + CLASSPATH=$jarfile:$CLASSPATH +done +for plugin in "@PLUGINJAVADIR@"/* ; do + if [ ! -e "$plugin" ] ; then continue ; fi + CLASSPATH=$plugin:$CLASSPATH +done +export CLASSPATH + +set -e +cd "@AGENTLIBDIR@" +echo Current directory is "$PWD" +echo CLASSPATH to run the agent: "$CLASSPATH" + +export PATH=/sbin:/usr/sbin:"$PATH" +SERVICEARGS= +for x in private public ; do + configuration=`grep -q "^$x.network.device" "@AGENTSYSCONFDIR@"/agent.properties || true` + if [ -n "$CONFIGURATION" ] ; then + echo "Using manually-configured network device $CONFIGURATION" + else + defaultroute=`ip route | grep ^default | cut -d ' ' -f 5` + test -n "$defaultroute" + echo "Using auto-discovered network device $defaultroute which is the default route" + SERVICEARGS="$SERVICEARGS $x.network.device="$defaultroute + fi +done + +function termagent() { + if [ "$agentpid" != "" ] ; then + echo Killing VMOps Agent "(PID $agentpid)" with SIGTERM >&2 + kill -TERM $agentpid + echo Waiting for agent to exit >&2 + wait $agentpid + ex=$? + echo Agent exited with return code $ex >&2 + else + echo Agent PID is unknown >&2 + fi +} + +trap termagent TERM +while true ; do + java -Xms128M -Xmx384M -cp "$CLASSPATH" "$@" com.cloud.agent.AgentShell $SERVICEARGS & + agentpid=$! + echo "Agent started. PID: $!" >&2 + wait $agentpid + ex=$? + if [ $ex -gt 128 ]; then + echo "wait on agent process interrupted by SIGTERM" >&2 + exit $ex + fi + echo "Agent exited with return code $ex" >&2 + if [ $ex -eq 0 ] || [ $ex -eq 1 ] || [ $ex -eq 66 ] || [ $ex -gt 128 ]; then + echo "Exiting..." > /dev/stderr + exit $ex + fi + echo "Restarting agent..." > /dev/stderr + sleep 1 +done diff --git a/agent/patch/patch.tgz b/agent/patch/patch.tgz new file mode 100644 index 0000000000000000000000000000000000000000..229f98ea9b7291ba0acf7d6543ec87a9dfd5eff8 GIT binary patch literal 2540 zcmV`$!kpFU7(_eu0s9xMc|ML8AzEsa#Up=$vNKu9{q3kHEn}HI8`VN4E$zYp2Mp@A&gv~L0XYy>2Vf(Y!uAzZKs9m4kwbNwNR zbrg%m94eq5!9bOije)D+nth`D=mf8!m2rF|8*9$LVeT%Tf78@-RbT>n-?G0T6XzqM(-7Cu50{THcrp2}QMh2?r>i(1HtUK4nuK$WQzLAoeJL z++;Q{M04==?HpG`SRDC)#o;ep{E(M9X#@sQFizKwQMmisQCVLw-ff|x;^M+4o=^Rq z)ZQAez1(9r$TwbD`H~>-hFHUrq8(ug#`T6V6Qx^J z%v@d!mcpEr9&AspS+NxgEz|b^keZV2V9@z1i_^fvwk}5EA z#Z=sruFvO^f4S4#8^Fje80SkD;T>O3BFvqUzN90#`bpYB_NsJ>@;GlzRyEbJ7}V9t z#8C!kmeWr&$buw+8yj-V&rISV@hJH|r52>)OFAdsifBufHwMVMAkJKP(nT$t{hMO5 zB>sJF&kHSwV@c`vN8}D+#<1{Hu($=MdrDErmCHc;yW9&1v!5FN-3R}bN*Vrp7UKDL zeU}xTr`ra1&%bW!yIKC9gJ$xNcRxgX`s3SJlYdhynG5+ZAt=c1|Ib02FJTG*Ho+d9 zOW}xpc5Nbzm$lSoM!l9UtBLco_Q-~?Yz-Jg8i4;OB!Ye8x5e0D*?u}e3 zZ_n8HD5Lxmt-8KM(6p8@I3+lEt?ikj3pUkwan0B*O4t>a#Mu;XtI;%gfxp2x8aBpSCtXsxI+%o3e zGVbCQyu%&v>EJWu-+e-m*2JnVf9{aGq~PO5!2>w6Gh|=zCyXHH-%0~`9P!M5D@23_ z-bIlcTPrim%kw;I*;r%Q5FP1`re}n1w=E09jQLHDhm#5d;-wQJUuR^8V0$itVmfWA zRQ0&z#E>1J^|ug;I`oTgSzbL!k0Qs+18%`PNQ4&s2lU|2c6rpPr;I&CfUN_`rlTQ2LG`ErqO(kj)e?<}^%O8^3 zBq#D~qG<>3zs$97bo@tOPBVIPO6w%^M9QDU1_ybSO8GYYrz7ZFz<-8mR5JYc9JC1^ z5M~e$B$)docc!5Av zWSr9uyriuwsuD zqmW-B5Pqea>H+wOZ!Oy6!}ra=tbX_v|f&|uoM1UH4kJw4(VJs2<+qu@#* zZV~l-q+}3#A{A3-#<(Q6z()L6A>0Y5%1c6sIZC|Yn))#%v>&?5IpDT%k}srp*v7>B zb`YHN7CVU_v%~&$Jk{{~Nefrdi5;iVLab_|3zWO9=!DLOY+=_t7D<6O}my1%$i&Cs;HsdapiN#{FhqP{=2u4)z2ELHxY z&2avn{4_T1{{Gh}l`7f$-{+wD{3Fuh<(d}oW*R=44blJ0xA%Wt+g<$o56#fB{{I}b z?ORN#wmXeZ>tpYz`3?>a4;$?sd@1kmS9i6&a^)*dx4ON>dA+)4;uMayKlT^T*(g_bQTVr3=hH!_p3G=fZCPQ(tnODTsIzbYQVn7o zFF;84pOj~AO1!ryjDe#X0)>M7>@+y&s_!(qy@OwmkGk(0^%V+?2Pg=^wa4LV??S*< zw``-?_{0arT)_#0h`}D7j;XF2{QQ1Z<#OqfH0^0Kijq|S=xvBvGtp03|Hb?AyVdB2Hi?J!h!rhjv9;Z5Q{%F+BdWCcND6Xg_wF+0if*u` zMI{2XPyhgv Ck{6=@ literal 0 HcmV?d00001 diff --git a/agent/patch/redopatch.sh b/agent/patch/redopatch.sh new file mode 100755 index 00000000000..fe70d29bbfc --- /dev/null +++ b/agent/patch/redopatch.sh @@ -0,0 +1,6 @@ +#!/bin/bash -x + +d=`dirname "$0"` +cd "$d" +tar c --owner root --group root -vzf patch.tgz root etc + diff --git a/agent/scripts/agent.sh b/agent/scripts/agent.sh new file mode 100755 index 00000000000..ebf7f4f8fc0 --- /dev/null +++ b/agent/scripts/agent.sh @@ -0,0 +1,13 @@ +#!/bin/bash +#run.sh runs the agent client. + +# set -x + +while true +do + ./run.sh "$@" + ex=$? + if [ $ex -eq 0 ] || [ $ex -eq 1 ] || [ $ex -eq 66 ] || [ $ex -gt 128 ]; then + exit $ex + fi +done diff --git a/agent/scripts/run.sh b/agent/scripts/run.sh new file mode 100755 index 00000000000..3acaf64f128 --- /dev/null +++ b/agent/scripts/run.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +#run.sh runs the agent client. +java $1 -Xms128M -Xmx384M -cp cglib-nodep-2.2.jar:xenserver-5.5.0-1.jar:trilead-ssh2-build213.jar:cloud-api.jar:cloud-core-extras.jar:cloud-utils.jar:cloud-agent.jar:cloud-console-proxy.jar:cloud-console-common.jar:freemarker.jar:log4j-1.2.15.jar:ws-commons-util-1.0.2.jar:xmlrpc-client-3.1.3.jar:cloud-core.jar:xmlrpc-common-3.1.3.jar:javaee-api-5.0-1.jar:gson-1.3.jar:commons-httpclient-3.1.jar:commons-logging-1.1.1.jar:commons-codec-1.4.jar:commons-collections-3.2.1.jar:commons-pool-1.4.jar:apache-log4j-extras-1.0.jar:libvirt-0.4.5.jar:jna.jar:.:/etc/cloud:./conf com.cloud.agent.AgentShell diff --git a/agent/src/com/cloud/agent/Agent.java b/agent/src/com/cloud/agent/Agent.java new file mode 100755 index 00000000000..6d3f9df9403 --- /dev/null +++ b/agent/src/com/cloud/agent/Agent.java @@ -0,0 +1,801 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.agent.api.AgentControlAnswer; +import com.cloud.agent.api.AgentControlCommand; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.CronCommand; +import com.cloud.agent.api.ModifySshKeysCommand; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.ShutdownCommand; +import com.cloud.agent.api.StartupAnswer; +import com.cloud.agent.api.StartupCommand; +import com.cloud.agent.api.UpgradeAnswer; +import com.cloud.agent.api.UpgradeCommand; +import com.cloud.agent.transport.Request; +import com.cloud.agent.transport.Response; +import com.cloud.agent.transport.UpgradeResponse; +import com.cloud.exception.AgentControlChannelException; +import com.cloud.resource.ServerResource; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.backoff.BackoffAlgorithm; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.nio.HandlerFactory; +import com.cloud.utils.nio.Link; +import com.cloud.utils.nio.NioClient; +import com.cloud.utils.nio.NioConnection; +import com.cloud.utils.nio.Task; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; + +/** + * @config + * {@table + * || Param Name | Description | Values | Default || + * || type | Type of server | Storage / Computing / Routing | No Default || + * || workers | # of workers to process the requests | int | 1 || + * || host | host to connect to | ip address | localhost || + * || port | port to connect to | port number | 8250 || + * || instance | Used to allow multiple agents running on the same host | String | none || + * } + * + * For more configuration options, see the individual types. + * + **/ +public class Agent implements HandlerFactory, IAgentControl { + private static final Logger s_logger = Logger.getLogger(Agent.class.getName()); + + public enum ExitStatus { + Normal(0), // Normal status = 0. + Upgrade(65), // Exiting for upgrade. + Configuration(66), // Exiting due to configuration problems. + Error(67); // Exiting because of error. + + int value; + ExitStatus(final int value) { + this.value = value; + } + + public int value() { + return value; + } + } + + List _controlListeners = new ArrayList(); + + IAgentShell _shell; + NioConnection _connection; + ServerResource _resource; + Link _link; + Long _id; + + Timer _timer = new Timer("Agent Timer"); + + List _watchList = new ArrayList(); + long _sequence = 0; + long _lastPingResponseTime = 0; + long _pingInterval = 0; + AtomicInteger _inProgress = new AtomicInteger(); + + StartupTask _startup = null; + + // for simulator use only + public Agent(IAgentShell shell) { + _shell = shell; + _link = null; + + _connection = new NioClient( + "Agent", + _shell.getHost(), + _shell.getPort(), + _shell.getWorkers(), + this); + + Runtime.getRuntime().addShutdownHook(new ShutdownThread(this)); + } + + public Agent(IAgentShell shell, int localAgentId, ServerResource resource) throws ConfigurationException { + _shell = shell; + _resource = resource; + _link = null; + + resource.setAgentControl(this); + + String value = _shell.getPersistentProperty(getResourceName(), "id"); + _id = value != null ? Long.parseLong(value) : null; + s_logger.info("id is " + ((_id != null) ? _id : "")); + + final Map params = PropertiesUtil.toMap(_shell.getProperties()); + + // merge with properties from command line to let resource access command line parameters + for(Map.Entry cmdLineProp : _shell.getCmdLineProperties().entrySet()) { + params.put(cmdLineProp.getKey(), cmdLineProp.getValue()); + } + + if (!_resource.configure(getResourceName(), params)) { + throw new ConfigurationException("Unable to configure " + _resource.getName()); + } + + _connection = new NioClient( + "Agent", + _shell.getHost(), + _shell.getPort(), + _shell.getWorkers(), + this); + + // ((NioClient)_connection).setBindAddress(_shell.getPrivateIp()); + + s_logger.debug("Adding shutdown hook"); + Runtime.getRuntime().addShutdownHook(new ShutdownThread(this)); + + s_logger.info("Agent [id = " + (_id != null ? _id : "new") + " : type = " + getResourceName() + + " : zone = " + _shell.getZone() + " : pod = " + _shell.getPod() + + " : workers = " + _shell.getWorkers() + " : host = " + _shell.getHost() + + " : port = " + _shell.getPort()); + } + + public String getVersion() { + return _shell.getVersion(); + } + + public String getResourceGuid() { + String guid = _shell.getGuid(); + return guid + "-" + getResourceName(); + } + + public String getZone() { + return _shell.getZone(); + } + + public String getPod() { + return _shell.getPod(); + } + + protected void setLink(final Link link) { + _link = link; + } + + public ServerResource getResource() { + return _resource; + } + + public BackoffAlgorithm getBackoffAlgorithm() { + return _shell.getBackoffAlgorithm(); + } + + public String getResourceName() { + return _resource.getClass().getSimpleName(); + } + + public void upgradeAgent(final String url, boolean protocol) { + // shell needs to take care of synchronization when multiple-instances demand upgrade + // at the same time + _shell.upgradeAgent(url); + + // To stop agent after it has been upgraded, as shell executor may prematurely time out + // tasks if agent is in shutting down process + if (protocol) { + if (_connection != null) { + _connection.stop(); + _connection = null; + } + if (_resource != null) { + _resource.stop(); + _resource = null; + } + } else { + stop(ShutdownCommand.Update, null); + } + } + + public void start() { + if (!_resource.start()) { + s_logger.error("Unable to start the resource: " + _resource.getName()); + throw new CloudRuntimeException("Unable to start the resource: " + _resource.getName()); + } + _connection.start(); + } + + public void stop(final String reason, final String detail) { + s_logger.info("Stopping the agent: Reason = " + reason + (detail != null ? ": Detail = " + detail : "")); + if (_connection != null) { + final ShutdownCommand cmd = new ShutdownCommand(reason, detail); + try { + if (_link != null) { + Request req = new Request(0, (_id != null? _id : -1), -1, cmd, false); + _link.send(req.toBytes()); + } + } catch (final ClosedChannelException e) { + s_logger.warn("Unable to send: " + cmd.toString()); + } catch(Exception e) { + s_logger.warn("Unable to send: " + cmd.toString() + " due to exception: ", e); + } + s_logger.debug("Sending shutdown to management server"); + try { + Thread.sleep(1000); + } catch (final InterruptedException e) { + s_logger.debug("Who the heck interrupted me here?"); + } + _connection.stop(); + _connection = null; + } + + if (_resource != null) { + _resource.stop(); + _resource = null; + } + } + + public Long getId() { + return _id; + } + + public void setId(final Long id) { + s_logger.info("Set agent id " + id); + _id = id; + _shell.setPersistentProperty(getResourceName(), "id", Long.toString(id)); + } + + public void scheduleWatch(final Link link, final Request request, final long delay, final long period) { + synchronized (_watchList) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Adding a watch list"); + } + final WatchTask task = new WatchTask(link, request, this); + _timer.schedule(task, delay, period); + _watchList.add(task); + } + } + + protected void cancelTasks() { + synchronized(_watchList) { + for (final WatchTask task : _watchList) { + task.cancel(); + } + if (s_logger.isDebugEnabled()) { + s_logger.debug("Clearing watch list: " + _watchList.size()); + } + _watchList.clear(); + } + } + + public void sendStartup(Link link) { + final StartupCommand[] startup = _resource.initialize(); + final Command[] commands = new Command[startup.length]; + for (int i=0; i< startup.length; i++){ + setupStartupCommand(startup[i]); + commands[i] = startup[i]; + } + + final Request request = new Request(getNextSequence(), _id != null ? _id : -1, -1, commands, false); + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Sending Startup: " + request.toString()); + } + synchronized(this) { + _startup = new StartupTask(link); + _timer.schedule(_startup, 180000); + } + try { + link.send(request.toBytes()); + } catch (final ClosedChannelException e) { + s_logger.warn("Unable to send reques: " + request.toString()); + } + } + + protected void setupStartupCommand(StartupCommand startup) { + InetAddress addr; + try { + addr = InetAddress.getLocalHost(); + } catch (final UnknownHostException e) { + s_logger.warn("unknow host? ", e); + //ignore + return; + } + + final Script command = new Script("hostname", 500, s_logger); + final OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + final String result = command.execute(parser); + final String hostname = result == null ? parser.getLine() : addr.toString(); + + startup.setId(getId()); + if (startup.getName() == null) + startup.setName(hostname); + startup.setDataCenter(getZone()); + startup.setPod(getPod()); + startup.setGuid(getResourceGuid()); + startup.setResourceName(getResourceName()); + startup.setVersion(getVersion()); + } + + @Override + public Task create(Task.Type type, Link link, byte[] data) { + return new ServerHandler(type, link, data); + } + + protected void reconnect(final Link link) { + synchronized(this) { + if (_startup != null) { + _startup.cancel(); + _startup= null; + } + } + + link.close(); + link.terminated(); + + setLink(null); + cancelTasks(); + + _resource.disconnected(); + + while (true) { + _shell.getBackoffAlgorithm().waitBeforeRetry(); + + s_logger.info("Lost connection to the server. Reconnecting...."); + + int inProgress = 0; + if ((inProgress = _inProgress.get()) > 0) { + s_logger.info("Cannot connect because we still have " + inProgress + " commands in progress."); + continue; + } + + try { + final SocketChannel sch = SocketChannel.open(); + sch.configureBlocking(false); + sch.connect(link.getSocketAddress()); + + link.connect(sch); + return; + } catch(final IOException e) { + s_logger.error("Unable to establish connection with the server", e); + } + } + } + + public void processStartupAnswer(Answer answer, Response response, Link link) { + boolean cancelled = false; + synchronized(this) { + if (_startup != null) { + _startup.cancel(); + _startup = null; + } else { + cancelled = true; + } + } + + final StartupAnswer startup = (StartupAnswer)answer; + if (!startup.getResult()) { + s_logger.error("Not allowed to connect to the server: " + answer.getDetails()); + System.exit(1); + } + if (cancelled) { + s_logger.warn("Threw away a startup answer because we're reconnecting."); + return; + } + setId(startup.getHostId()); + _pingInterval = startup.getPingInterval() * 1000; // change to ms. + + setLastPingResponseTime(); + scheduleWatch(link, response, _pingInterval, _pingInterval); + s_logger.info("Startup Response Received: agent id = " + getId()); + } + + protected void processRequest(final Request request, final Link link) { + boolean requestLogged = false; + Response response = null; + try { + final Command[] cmds = request.getCommands(); + final Answer[] answers = new Answer[cmds.length]; + + for (int i = 0; i < cmds.length; i++) + { + final Command cmd = cmds[i]; + Answer answer; + try + { + if (s_logger.isDebugEnabled()) + { + //this is a hack to make sure we do NOT log the ssh keys + if((cmd instanceof ModifySshKeysCommand)) + { + s_logger.debug("Received the request for command: ModifySshKeysCommand"); + } + else + { + if(!requestLogged) //ensures request is logged only once per method call + { + s_logger.debug("Request:" + request.toString()); + requestLogged = true; + } + } + + s_logger.debug("Processing command: " + cmd.toString()); + } + + if (cmd instanceof CronCommand) { + final CronCommand watch = (CronCommand)cmd; + scheduleWatch(link, request, watch.getInterval() * 1000, watch.getInterval() * 1000); + answer = new Answer(cmd, true, null); + } else if (cmd instanceof UpgradeCommand) { + final UpgradeCommand upgrade = (UpgradeCommand)cmd; + answer = upgradeAgent(upgrade.getUpgradeUrl(), upgrade); + } else if(cmd instanceof AgentControlCommand) { + answer = null; + synchronized(_controlListeners) { + for(IAgentControlListener listener: _controlListeners) { + answer = listener.processControlRequest(request, (AgentControlCommand)cmd); + if(answer != null) + break; + } + } + + if(answer == null) { + s_logger.warn("No handler found to process cmd: " + cmd.toString()); + answer = new AgentControlAnswer(cmd); + } + + } else { + _inProgress.incrementAndGet(); + try { + answer = _resource.executeRequest(cmd); + } finally { + _inProgress.decrementAndGet(); + } + if (answer == null) { + s_logger.debug("Response: unsupported command" + cmd.toString()); + answer = Answer.createUnsupportedCommandAnswer(cmd); + } + } + } catch (final Throwable th) { + s_logger.warn("Caught: ", th); + final StringWriter writer = new StringWriter(); + th.printStackTrace(new PrintWriter(writer)); + answer = new Answer(cmd, false, writer.toString()); + } + + answers[i] = answer; + if (!answer.getResult() && request.stopOnError()) { + for (i++; i < cmds.length; i++) { + answers[i] = new Answer(cmds[i], false, "Stopped by previous failure"); + } + break; + } + } + response = new Response(request, answers); + } finally { + if (s_logger.isDebugEnabled()) { + s_logger.debug(response != null ? response.toString() : "response is null"); + } + + if (response != null) { + try { + link.send(response.toBytes()); + } catch (final ClosedChannelException e) { + s_logger.warn("Unable to send response: " + response.toString()); + } + } + } + } + + public void processResponse(final Response response, final Link link) { + final Answer answer = response.getAnswer(); + if (s_logger.isDebugEnabled()) { + s_logger.debug("Received response: " + response.toString()); + } + if (answer instanceof StartupAnswer) { + processStartupAnswer(answer, response, link); + } else if(answer instanceof AgentControlAnswer) { + // Notice, we are doing callback while holding a lock! + synchronized(_controlListeners) { + for(IAgentControlListener listener : _controlListeners) { + listener.processControlResponse(response, (AgentControlAnswer)answer); + } + } + } else { + setLastPingResponseTime(); + } + } + + public void processOtherTask(Task task) { + final Object obj = task.get(); + if (obj instanceof Response) { + if ((System.currentTimeMillis() - _lastPingResponseTime) > _pingInterval * _shell.getPingRetries()) { + s_logger.error("Ping Interval has gone past " + _pingInterval * _shell.getPingRetries() + ". Attempting to reconnect."); + final Link link = task.getLink(); + reconnect(link); + return; + } + + final PingCommand ping = _resource.getCurrentStatus(getId()); + final Request request = new Request(getNextSequence(), _id, -1, ping, false); + if (s_logger.isDebugEnabled()) { + s_logger.debug("Sending ping: " + request.toString()); + } + + try { + task.getLink().send(request.toBytes()); + } catch (final ClosedChannelException e) { + s_logger.warn("Unable to send request: " + request.toString()); + } + } else if (obj instanceof Request){ + final Request req = (Request)obj; + final Command command = req.getCommand(); + Answer answer = null; + _inProgress.incrementAndGet(); + try { + answer = _resource.executeRequest(command); + } finally { + _inProgress.decrementAndGet(); + } + if (answer != null) { + final Response response = new Response(req, answer); + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Watch Sent: " + response.toString()); + } + try { + task.getLink().send(response.toBytes()); + } catch (final ClosedChannelException e) { + s_logger.warn("Unable to send response: " + response.toString()); + } + } + } else { + s_logger.warn("Ignoring an unknown task"); + } + } + + protected UpgradeAnswer upgradeAgent(final String url, final UpgradeCommand cmd) { + try { + upgradeAgent(url, cmd == null); + return null; + } catch(final Exception e) { + s_logger.error("Unable to run this agent because we couldn't complete the upgrade process.", e); + if (cmd != null) { + final StringWriter writer = new StringWriter(); + writer.append(e.getMessage()); + writer.append("===>Stack<==="); + e.printStackTrace(new PrintWriter(writer)); + return new UpgradeAnswer(cmd, writer.toString()); + } + + System.exit(3); + return null; + } + } + + public synchronized void setLastPingResponseTime() { + _lastPingResponseTime = System.currentTimeMillis(); + } + + protected synchronized long getNextSequence() { + return _sequence++; + } + + @Override + public void registerControlListener(IAgentControlListener listener) { + synchronized(_controlListeners) { + _controlListeners.add(listener); + } + } + + @Override + public void unregisterControlListener(IAgentControlListener listener) { + synchronized(_controlListeners) { + _controlListeners.remove(listener); + } + } + + @Override + public AgentControlAnswer sendRequest(AgentControlCommand cmd, int timeoutInMilliseconds) throws AgentControlChannelException { + Request request = new Request(this.getNextSequence(), this.getId(), + -1, new Command[] {cmd}, true, false); + AgentControlListener listener = new AgentControlListener(request); + + registerControlListener(listener); + try { + postRequest(request); + synchronized(listener) { + try { + listener.wait(timeoutInMilliseconds); + } catch (InterruptedException e) { + s_logger.warn("sendRequest is interrupted, exit waiting"); + } + } + + return listener.getAnswer(); + } finally { + unregisterControlListener(listener); + } + } + + @Override + public void postRequest(AgentControlCommand cmd) throws AgentControlChannelException { + Request request = new Request(this.getNextSequence(), this.getId(), + -1, new Command[] {cmd}, true, false); + postRequest(request); + } + + private void postRequest(Request request) throws AgentControlChannelException { + if(_link != null) { + try { + _link.send(request.toBytes()); + } catch (final ClosedChannelException e) { + s_logger.warn("Unable to post agent control reques: " + request.toString()); + throw new AgentControlChannelException("Unable to post agent control request due to " + e.getMessage()); + } + } else { + throw new AgentControlChannelException("Unable to post agent control request as link is not available"); + } + } + + public class AgentControlListener implements IAgentControlListener { + private AgentControlAnswer _answer; + private final Request _request; + + public AgentControlListener(Request request) { + _request = request; + } + + public AgentControlAnswer getAnswer() { + return _answer; + } + + public Answer processControlRequest(Request request, AgentControlCommand cmd) { + return null; + } + + public void processControlResponse(Response response, AgentControlAnswer answer) { + if(_request.getSequence() == response.getSequence()) { + _answer = answer; + synchronized(this) { + notifyAll(); + } + } + } + } + + protected class ShutdownThread extends Thread { + Agent _agent; + public ShutdownThread(final Agent agent) { + super("AgentShutdownThread"); + _agent = agent; + } + @Override + public void run() { + _agent.stop(ShutdownCommand.Requested, null); + } + } + + public class WatchTask extends TimerTask { + protected Request _request; + protected Agent _agent; + protected Link _link; + public WatchTask(final Link link, final Request request, final Agent agent) { + super(); + _request = request; + _link = link; + _agent = agent; + } + + @Override + public void run() { + if (s_logger.isTraceEnabled()) { + s_logger.trace("Scheduling " + (_request instanceof Response ? "Ping" : "Watch Task")); + } + try { + _link.schedule(new ServerHandler(Task.Type.OTHER, _link, _request)); + } catch (final ClosedChannelException e) { + s_logger.warn("Unable to schedule task because channel is closed"); + } + } + } + + public class StartupTask extends TimerTask { + protected Link _link; + protected volatile boolean cancelled = false; + + public StartupTask(final Link link) { + s_logger.debug("Startup task created"); + _link = link; + } + + @Override + public synchronized boolean cancel() { + // TimerTask.cancel may fail depends on the calling context + if (!cancelled) { + cancelled = true; + s_logger.debug("Startup task cancelled"); + return super.cancel(); + } + return true; + } + + @Override + public synchronized void run() { + if(!cancelled) { + if(s_logger.isInfoEnabled()) { + s_logger.info("The startup command is now cancelled"); + } + cancelled = true; + _startup = null; + reconnect(_link); + } + } + } + + public class ServerHandler extends Task { + public ServerHandler(Task.Type type, Link link, byte[] data) { + super(type, link, data); + } + + public ServerHandler(Task.Type type, Link link, Request req) { + super(type, link, req); + } + + @Override + public void doTask(final Task task) { + if (task.getType() == Task.Type.CONNECT) { + _shell.getBackoffAlgorithm().reset(); + setLink(task.getLink()); + sendStartup(task.getLink()); + } else if (task.getType() == Task.Type.DATA) { + Request request; + try { + request = Request.parse(task.getData()); + if (request instanceof UpgradeResponse) { + upgradeAgent(((UpgradeResponse)request).getUpgradeUrl(), null); + } else if (request instanceof Response) { + processResponse((Response)request, task.getLink()); + } else { + processRequest(request, task.getLink()); + } + } catch (final ClassNotFoundException e) { + s_logger.error("Unable to find this request "); + } catch (final Exception e) { + s_logger.error("Error parsing task", e); + } + } else if (task.getType() == Task.Type.DISCONNECT) { + reconnect(task.getLink()); + return; + } else if (task.getType() == Task.Type.OTHER) { + processOtherTask(task); + } + } + } +} diff --git a/agent/src/com/cloud/agent/AgentShell.java b/agent/src/com/cloud/agent/AgentShell.java new file mode 100644 index 00000000000..49c9dc5f2cd --- /dev/null +++ b/agent/src/com/cloud/agent/AgentShell.java @@ -0,0 +1,590 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.naming.ConfigurationException; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.log4j.Logger; + +import com.cloud.agent.Agent.ExitStatus; +import com.cloud.agent.dao.StorageComponent; +import com.cloud.agent.dao.impl.PropertiesStorage; +import com.cloud.host.Host; +import com.cloud.resource.ServerResource; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.ProcessUtil; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.backoff.BackoffAlgorithm; +import com.cloud.utils.backoff.impl.ConstantTimeBackoff; +import com.cloud.utils.component.Adapters; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.MacAddress; +import com.cloud.utils.script.Script; + +public class AgentShell implements IAgentShell { + private static final Logger s_logger = Logger.getLogger(AgentShell.class.getName()); + + private final Properties _properties = new Properties(); + private final Map _cmdLineProperties = new HashMap(); + private StorageComponent _storage; + private BackoffAlgorithm _backoff; + private String _version; + private String _zone; + private String _pod; + private String _host; + private String _privateIp; + private int _port; + private int _proxyPort; + private int _workers; + private String _guid; + private int _nextAgentId = 1; + private volatile boolean _exit = false; + private int _pingRetries; + private Thread _consoleProxyMain = null; + private final List _agents = new ArrayList(); + + public AgentShell() { + } + + @Override + public Properties getProperties() { + return _properties; + } + + @Override + public BackoffAlgorithm getBackoffAlgorithm() { + return _backoff; + } + + @Override + public int getPingRetries() { + return _pingRetries; + } + + @Override + public String getVersion() { + return _version; + } + + @Override + public String getZone() { + return _zone; + } + + @Override + public String getPod() { + return _pod; + } + + @Override + public String getHost() { + return _host; + } + + @Override + public String getPrivateIp() { + return _privateIp; + } + + @Override + public int getPort() { + return _port; + } + + @Override + public int getProxyPort() { + return _proxyPort; + } + + @Override + public int getWorkers() { + return _workers; + } + + @Override + public String getGuid() { + return _guid; + } + + public Map getCmdLineProperties() { + return _cmdLineProperties; + } + + public String getProperty(String prefix, String name) { + if(prefix != null) + return _properties.getProperty(prefix + "." + name); + + return _properties.getProperty(name); + } + + @Override + public String getPersistentProperty(String prefix, String name) { + if(prefix != null) + return _storage.get(prefix + "." + name); + return _storage.get(name); + } + + @Override + public void setPersistentProperty(String prefix, String name, String value) { + if(prefix != null) + _storage.persist(prefix + "." + name, value); + else + _storage.persist(name, value); + } + + @Override + public void upgradeAgent(final String url) { + s_logger.info("Updating agent with binary from " + url); + synchronized(this) { + final Class c = this.getClass(); + String path = c.getResource(c.getSimpleName() + ".class").toExternalForm(); + final int begin = path.indexOf(File.separator); + int end = path.lastIndexOf("!"); + end = path.lastIndexOf(File.separator, end); + path = path.substring(begin, end); + + s_logger.debug("Current binaries reside at " + path); + + File file = null; + try { + file = File.createTempFile("agent-", "-" + Long.toString(new Date().getTime())); + wget(url, file); + } catch (final IOException e) { + s_logger.warn("Exception while downloading agent update package, ", e); + throw new CloudRuntimeException("Unable to update from " + url + ", exception:" + e.getMessage(), e); + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Unzipping " + file.getAbsolutePath() + " to " + path); + } + + final Script unzip = new Script("unzip", 120000, s_logger); + unzip.add("-o", "-q"); // overwrite and quiet + unzip.add(file.getAbsolutePath()); + unzip.add("-d", path); + + final String result = unzip.execute(); + if (result != null) { + throw new CloudRuntimeException("Unable to unzip the retrieved file: " + result); + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Closing the connection to the management server"); + } + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Exiting to start the new agent."); + } + System.exit(ExitStatus.Upgrade.value()); + } + + public static void wget(String url, File file) throws IOException { + final HttpClient client = new HttpClient(); + final GetMethod method = new GetMethod(url); + int response; + response = client.executeMethod(method); + if (response != HttpURLConnection.HTTP_OK) { + s_logger.warn("Retrieving from " + url + " gives response code: " + response); + throw new CloudRuntimeException("Unable to download from " + url + ". Response code is " + response); + } + + final InputStream is = method.getResponseBodyAsStream(); + s_logger.debug("Downloading content into " + file.getAbsolutePath()); + + final FileOutputStream fos = new FileOutputStream(file); + byte[] buffer = new byte[4096]; + int len = 0; + while( (len = is.read(buffer)) > 0) + fos.write(buffer, 0, len); + fos.close(); + + try { + is.close(); + } catch(IOException e) { + s_logger.warn("Exception while closing download stream from " + url + ", ", e); + } + } + + private void loadProperties() throws ConfigurationException { + final File file = PropertiesUtil.findConfigFile("agent.properties"); + if (file == null) { + throw new ConfigurationException("Unable to find agent.properties."); + } + + s_logger.info("agent.properties found at " + file.getAbsolutePath()); + + try { + _properties.load(new FileInputStream(file)); + } catch (final FileNotFoundException ex) { + throw new CloudRuntimeException("Cannot find the file: " + file.getAbsolutePath(), ex); + } catch (final IOException ex) { + throw new CloudRuntimeException("IOException in reading " + file.getAbsolutePath(), ex); + } + } + + protected boolean parseCommand(final String[] args) throws ConfigurationException { + String host = null; + String workers = null; + String port = null; + String zone = null; + String pod = null; + String guid = null; + for (int i = 0; i < args.length; i++) { + final String[] tokens = args[i].split("="); + if (tokens.length != 2) { + System.out.println("Invalid Parameter: " + args[i]); + continue; + } + + // save command line properties + _cmdLineProperties.put(tokens[0], tokens[1]); + + if (tokens[0].equalsIgnoreCase("port")) { + port = tokens[1]; + } else if (tokens[0].equalsIgnoreCase("threads")) { + workers = tokens[1]; + } else if (tokens[0].equalsIgnoreCase("host")) { + host = tokens[1]; + } else if(tokens[0].equalsIgnoreCase("zone")) { + zone = tokens[1]; + } else if(tokens[0].equalsIgnoreCase("pod")) { + pod = tokens[1]; + } else if(tokens[0].equalsIgnoreCase("guid")) { + guid = tokens[1]; + } else if(tokens[0].equalsIgnoreCase("eth1ip")) { + _privateIp = tokens[1]; + } + } + + if (port == null) { + port = getProperty(null, "port"); + } + + _port = NumbersUtil.parseInt(port, 8250); + + _proxyPort = NumbersUtil.parseInt(getProperty(null, "consoleproxy.httpListenPort"), 443); + + if (workers == null) { + workers = getProperty(null, "workers"); + } + + _workers = NumbersUtil.parseInt(workers, 5); + + if (host == null) { + host = getProperty(null, "host"); + } + + if (host == null) { + host = "localhost"; + } + _host = host; + + if(zone != null) + _zone = zone; + else + _zone = getProperty(null, "zone"); + if (_zone == null || (_zone.startsWith("@") && _zone.endsWith("@"))) { + _zone = "default"; + } + + if(pod != null) + _pod = pod; + else + _pod = getProperty(null, "pod"); + if (_pod == null || (_pod.startsWith("@") && _pod.endsWith("@"))) { + _pod = "default"; + } + + if (_host == null || (_host.startsWith("@") && _host.endsWith("@"))) { + throw new ConfigurationException("Host is not configured correctly: " + _host); + } + + final String retries = getProperty(null, "ping.retries"); + _pingRetries = NumbersUtil.parseInt(retries, 5); + + String value = getProperty(null, "developer"); + boolean developer = Boolean.parseBoolean(value); + + if(guid != null) + _guid = guid; + else + _guid = getProperty(null, "guid"); + if (_guid == null) { + if (!developer) { + throw new ConfigurationException("Unable to find the guid"); + } + _guid = MacAddress.getMacAddress().toString(":"); + } + + return true; + } + + private void init(String[] args) throws ConfigurationException{ + + final ComponentLocator locator = ComponentLocator.getLocator("agent"); + + final Class c = this.getClass(); + _version = c.getPackage().getImplementationVersion(); + if (_version == null) { + throw new CloudRuntimeException("Unable to find the implementation version of this agent"); + } + s_logger.info("Implementation Version is " + _version); + + parseCommand(args); + + _storage = locator.getManager(StorageComponent.class); + if (_storage == null) { + s_logger.info("Defaulting to using properties file for storage"); + _storage = new PropertiesStorage(); + _storage.configure("Storage", new HashMap()); + } + + + // merge with properties from command line to let resource access command line parameters + for(Map.Entry cmdLineProp : getCmdLineProperties().entrySet()) { + _properties.put(cmdLineProp.getKey(), cmdLineProp.getValue()); + } + + final Adapters adapters = locator.getAdapters(BackoffAlgorithm.class); + final Enumeration en = adapters.enumeration(); + while (en.hasMoreElements()) { + _backoff = (BackoffAlgorithm)en.nextElement(); + break; + } + if (en.hasMoreElements()) { + s_logger.info("More than one backoff algorithm specified. Using the first one "); + } + + if (_backoff == null) { + s_logger.info("Defaulting to the constant time backoff algorithm"); + _backoff = new ConstantTimeBackoff(); + _backoff.configure("ConstantTimeBackoff", new HashMap()); + } + } + + private void launchAgent() throws ConfigurationException { + String resourceClassNames = getProperty(null, "resource"); + s_logger.trace("resource=" + resourceClassNames); + if(resourceClassNames != null) { + launchAgentFromClassInfo(resourceClassNames); + return; + } + + launchAgentFromTypeInfo(); + } + + private boolean needConsoleProxy() { + for(Agent agent: _agents) { + if( agent.getResource().getType().equals(Host.Type.ConsoleProxy)|| + agent.getResource().getType().equals(Host.Type.Routing)) + return true; + } + return false; + } + + private int getConsoleProxyPort() { + int port = NumbersUtil.parseInt(getProperty(null, "consoleproxy.httpListenPort"), 443); + return port; + } + + private void openPortWithIptables(int port) { + // TODO + } + + private void launchConsoleProxy() throws ConfigurationException { + if(!needConsoleProxy()) { + if(s_logger.isInfoEnabled()) + s_logger.info("Storage only agent, no need to start console proxy on it"); + return; + } + + int port = getConsoleProxyPort(); + openPortWithIptables(port); + + _consoleProxyMain = new Thread(new Runnable() { + public void run() { + try { + Class consoleProxyClazz = Class.forName("com.cloud.consoleproxy.ConsoleProxy"); + + try { + Method method = consoleProxyClazz.getMethod("start", Properties.class); + method.invoke(null, _properties); + } catch (SecurityException e) { + s_logger.error("Unable to launch console proxy due to SecurityException"); + System.exit(ExitStatus.Error.value()); + } catch (NoSuchMethodException e) { + s_logger.error("Unable to launch console proxy due to NoSuchMethodException"); + System.exit(ExitStatus.Error.value()); + } catch (IllegalArgumentException e) { + s_logger.error("Unable to launch console proxy due to IllegalArgumentException"); + System.exit(ExitStatus.Error.value()); + } catch (IllegalAccessException e) { + s_logger.error("Unable to launch console proxy due to IllegalAccessException"); + System.exit(ExitStatus.Error.value()); + } catch (InvocationTargetException e) { + s_logger.error("Unable to launch console proxy due to InvocationTargetException"); + System.exit(ExitStatus.Error.value()); + } + } catch (final ClassNotFoundException e) { + s_logger.error("Unable to launch console proxy due to ClassNotFoundException"); + System.exit(ExitStatus.Error.value()); + } + } + }, "Console-Proxy-Main"); + _consoleProxyMain.setDaemon(true); + _consoleProxyMain.start(); + } + + private void launchAgentFromClassInfo(String resourceClassNames) throws ConfigurationException { + String[] names = resourceClassNames.split("\\|"); + for(String name: names) { + Class impl; + try { + impl = Class.forName(name); + final Constructor constructor = impl.getDeclaredConstructor(); + constructor.setAccessible(true); + ServerResource resource = (ServerResource)constructor.newInstance(); + launchAgent(getNextAgentId(), resource); + } catch (final ClassNotFoundException e) { + throw new ConfigurationException("Resource class not found: " + name); + } catch (final SecurityException e) { + throw new ConfigurationException("Security excetion when loading resource: " + name); + } catch (final NoSuchMethodException e) { + throw new ConfigurationException("Method not found excetion when loading resource: " + name); + } catch (final IllegalArgumentException e) { + throw new ConfigurationException("Illegal argument excetion when loading resource: " + name); + } catch (final InstantiationException e) { + throw new ConfigurationException("Instantiation excetion when loading resource: " + name); + } catch (final IllegalAccessException e) { + throw new ConfigurationException("Illegal access exception when loading resource: " + name); + } catch (final InvocationTargetException e) { + throw new ConfigurationException("Invocation target exception when loading resource: " + name); + } + } + } + + private void launchAgentFromTypeInfo() throws ConfigurationException { + String typeInfo = getProperty(null, "type"); + if (typeInfo == null) { + s_logger.error("Unable to retrieve the type"); + throw new ConfigurationException("Unable to retrieve the type of this agent."); + } + s_logger.trace("Launching agent based on type=" + typeInfo); + } + + private void launchAgent(int localAgentId, ServerResource resource) throws ConfigurationException { + // we don't track agent after it is launched for now + Agent agent = new Agent(this, localAgentId, resource); + _agents.add(agent); + agent.start(); + } + + public synchronized int getNextAgentId() { + return _nextAgentId++; + } + + private void run(String[] args) { + try { + System.setProperty("java.net.preferIPv4Stack","true"); + + loadProperties(); + init(args); + + String instance = getProperty(null, "instance"); + if (instance == null) { + instance = ""; + } else { + instance += "."; + } + final String run = "agent." + instance + "pid"; + s_logger.debug("Checking to see if " + run + "exists."); + ProcessUtil.pidCheck(run); + + launchAgent(); + + // + // For both KVM & Xen-Server hypervisor, we have switched to VM-based console proxy solution, disable launching + // of console proxy here + // + // launchConsoleProxy(); + // + + try { + while(!_exit) Thread.sleep(1000); + } catch(InterruptedException e) { + } + + } catch(final ConfigurationException e) { + s_logger.error("Unable to start agent: " + e.getMessage()); + System.out.println("Unable to start agent: " + e.getMessage()); + System.exit(ExitStatus.Configuration.value()); + } catch (final Exception e) { + s_logger.error("Unable to start agent: ", e); + System.out.println("Unable to start agent: " + e.getMessage()); + System.exit(ExitStatus.Error.value()); + } + } + + public void stop() { + _exit = true; + if(_consoleProxyMain != null) { + _consoleProxyMain.interrupt(); + } + } + + public static void main(String[] args) { + AgentShell shell = new AgentShell(); + Runtime.getRuntime().addShutdownHook(new ShutdownThread(shell)); + shell.run(args); + } + + private static class ShutdownThread extends Thread { + AgentShell _shell; + public ShutdownThread(AgentShell shell) { + this._shell = shell; + } + + @Override + public void run() { + _shell.stop(); + } + } +} diff --git a/agent/src/com/cloud/agent/IAgentShell.java b/agent/src/com/cloud/agent/IAgentShell.java new file mode 100644 index 00000000000..f622a463f38 --- /dev/null +++ b/agent/src/com/cloud/agent/IAgentShell.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent; + +import java.util.Map; +import java.util.Properties; + +import com.cloud.utils.backoff.BackoffAlgorithm; + +public interface IAgentShell { + public Map getCmdLineProperties(); + public Properties getProperties(); + public String getPersistentProperty(String prefix, String name); + public void setPersistentProperty(String prefix, String name, String value); + + public String getHost(); + public String getPrivateIp(); + public int getPort(); + public int getWorkers(); + public int getProxyPort(); + public String getGuid(); + public String getZone(); + public String getPod(); + + public BackoffAlgorithm getBackoffAlgorithm(); + public int getPingRetries(); + + public void upgradeAgent(final String url); + public String getVersion(); +} diff --git a/agent/src/com/cloud/agent/dao/StorageComponent.java b/agent/src/com/cloud/agent/dao/StorageComponent.java new file mode 100755 index 00000000000..79b81d144d6 --- /dev/null +++ b/agent/src/com/cloud/agent/dao/StorageComponent.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.dao; + +import com.cloud.utils.component.Manager; + + +/** + * StorageDao is an abstraction layer for what the agent will use for storage. + * + */ +public interface StorageComponent extends Manager { + String get(String key); + void persist(String key, String value); +} diff --git a/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java b/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java new file mode 100755 index 00000000000..2a925da7bd4 --- /dev/null +++ b/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.dao.impl; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.agent.dao.StorageComponent; +import com.cloud.utils.PropertiesUtil; + +/** + * Uses Properties to implement storage. + * + * @config + * {@table + * || Param Name | Description | Values | Default || + * || path | path to the properties _file | String | db/db.properties || + * } + **/ +@Local(value={StorageComponent.class}) +public class PropertiesStorage implements StorageComponent { + private static final Logger s_logger = Logger.getLogger(PropertiesStorage.class); + Properties _properties = new Properties(); + File _file; + String _name; + + @Override + public synchronized String get(String key) { + return _properties.getProperty(key); + } + + @Override + public synchronized void persist(String key, String value) { + _properties.setProperty(key, value); + FileOutputStream output = null; + try { + output = new FileOutputStream(_file); + _properties.store(output, _name); + output.flush(); + output.close(); + } catch (FileNotFoundException e) { + s_logger.error("Who deleted the file? ", e); + } catch (IOException e) { + s_logger.error("Uh-oh: ", e); + } finally { + if (output != null) { + try { + output.close(); + } catch (IOException e) { + //ignore. + } + } + } + } + + @Override + public boolean configure(String name, Map params) { + _name = name; + String path = (String)params.get("path"); + if (path == null) { + path = "agent.properties"; + } + + File file = PropertiesUtil.findConfigFile(path); + if (file == null) { + file = new File(path); + try { + if (!file.createNewFile()) { + s_logger.error("Unable to create _file: " + file.getAbsolutePath()); + return false; + } + } catch (IOException e) { + s_logger.error("Unable to create _file: " + file.getAbsolutePath(), e); + return false; + } + } + + try { + _properties.load(new FileInputStream(file)); + _file = file; + } catch (FileNotFoundException e) { + s_logger.error("How did we get here? ", e); + return false; + } catch (IOException e) { + s_logger.error("IOException: ", e); + return false; + } + + return true; + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } + +} diff --git a/agent/src/com/cloud/agent/resource/DummyResource.java b/agent/src/com/cloud/agent/resource/DummyResource.java new file mode 100755 index 00000000000..c269c64284d --- /dev/null +++ b/agent/src/com/cloud/agent/resource/DummyResource.java @@ -0,0 +1,104 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.resource; + +import java.util.Map; + +import javax.ejb.Local; + +import com.cloud.agent.IAgentControl; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.host.Host; +import com.cloud.host.Host.Type; +import com.cloud.resource.ServerResource; + +@Local(value={ServerResource.class}) +public class DummyResource implements ServerResource { + String _name; + Host.Type _type; + boolean _negative; + IAgentControl _agentControl; + + @Override + public void disconnected() { + } + + @Override + public Answer executeRequest(Command cmd) { + System.out.println("Received Command: " + cmd.toString()); + Answer answer = new Answer(cmd, !_negative, "response"); + System.out.println("Replying with: " + answer.toString()); + return answer; + } + + @Override + public PingCommand getCurrentStatus(long id) { + return new PingCommand(_type, id); + } + + @Override + public Type getType() { + return _type; + } + + @Override + public StartupCommand[] initialize() { + return new StartupCommand[] {new StartupCommand()}; + } + + @Override + public boolean configure(String name, Map params) { + _name = name; + + String value = (String)params.get("type"); + _type = Host.Type.valueOf(value); + + value = (String)params.get("negative.reply"); + _negative = Boolean.parseBoolean(value); + + return true; + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } + + @Override + public IAgentControl getAgentControl() { + return _agentControl; + } + + @Override + public void setAgentControl(IAgentControl agentControl) { + _agentControl = agentControl; + } +} diff --git a/agent/src/com/cloud/agent/resource/computing/LibvirtCapXMLParser.java b/agent/src/com/cloud/agent/resource/computing/LibvirtCapXMLParser.java new file mode 100644 index 00000000000..f99adcb063d --- /dev/null +++ b/agent/src/com/cloud/agent/resource/computing/LibvirtCapXMLParser.java @@ -0,0 +1,224 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.resource.computing; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; + +import org.apache.log4j.Logger; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * @author chiradeep + * + */ +public class LibvirtCapXMLParser extends LibvirtXMLParser { + private boolean _host = false; + private boolean _guest = false; + private boolean _osType = false; + private boolean _domainTypeKVM = false; + private boolean _emulatorFlag = false; + private String _emulator ; + private final StringBuffer _capXML = new StringBuffer(); + private static final Logger s_logger = Logger.getLogger(LibvirtCapXMLParser.class); + private final ArrayList guestOsTypes = new ArrayList(); + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + if(qName.equalsIgnoreCase("host")) { + _host = false; + } else if (qName.equalsIgnoreCase("os_type")) { + _osType = false; + } else if (qName.equalsIgnoreCase("guest")) { + _guest = false; + } else if (qName.equalsIgnoreCase("domain")) { + _domainTypeKVM = false; + } else if (qName.equalsIgnoreCase("emulator")) { + _emulatorFlag = false; + } else if (_host) { + _capXML.append("<").append("/").append(qName).append(">"); + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (_host) { + _capXML.append(ch, start, length); + } else if (_osType) { + guestOsTypes.add(new String(ch, start, length)); + } else if (_emulatorFlag) { + _emulator = new String(ch, start, length); + } + } + + + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws SAXException { + if(qName.equalsIgnoreCase("host")) { + _host = true; + } else if (qName.equalsIgnoreCase("guest")) { + _guest = true; + } else if (qName.equalsIgnoreCase("os_type")) { + if (_guest) { + _osType = true; + } + } else if (qName.equalsIgnoreCase("domain")) { + for (int i = 0; i < attributes.getLength(); i++) { + if (attributes.getQName(i).equalsIgnoreCase("type") + && attributes.getValue(i).equalsIgnoreCase("kvm")) { + _domainTypeKVM = true; + } + } + } else if (qName.equalsIgnoreCase("emulator") && _domainTypeKVM) { + _emulatorFlag = true; + } else if (_host) { + _capXML.append("<").append(qName); + for (int i=0; i < attributes.getLength(); i++) { + _capXML.append(" ").append(attributes.getQName(i)).append("=").append(attributes.getValue(i)); + } + _capXML.append(">"); + } + + } + + public String parseCapabilitiesXML(String capXML) { + if (!_initialized){ + return null; + } + try { + _sp.parse(new InputSource(new StringReader(capXML)), this); + return _capXML.toString(); + }catch(SAXException se) { + s_logger.warn(se.getMessage()); + }catch (IOException ie) { + s_logger.error(ie.getMessage()); + } + return null; + } + + public ArrayList getGuestOsType() { + return guestOsTypes; + } + + public String getEmulator() { + return _emulator; + } + + public static void main(String [] args) { + String capXML = ""+ + " "+ + " "+ + " x86_64"+ + " core2duo"+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " tcp"+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + ""+ + " "+ + " hvm"+ + " "+ + " 32"+ + " /usr/bin/qemu"+ + " pc-0.11"+ + " pc"+ + " pc-0.10"+ + " isapc"+ + " "+ + " "+ + " "+ + " /usr/bin/qemu-kvm"+ + " pc-0.11"+ + " pc"+ + " pc-0.10"+ + " isapc"+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " hvm"+ + " "+ + " 64"+ + " /usr/bin/qemu-system-x86_64"+ + " pc-0.11"+ + " pc"+ + " pc-0.10"+ + " isapc"+ + " "+ + " "+ + " "+ + " /usr/bin/qemu-kvm"+ + " pc-0.11"+ + " pc"+ + " pc-0.10"+ + " isapc"+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + ""; + + LibvirtCapXMLParser parser = new LibvirtCapXMLParser(); + String cap = parser.parseCapabilitiesXML(capXML); + System.out.println(parser.getGuestOsType()); + System.out.println(parser.getEmulator()); + } +} diff --git a/agent/src/com/cloud/agent/resource/computing/LibvirtComputingResource.java b/agent/src/com/cloud/agent/resource/computing/LibvirtComputingResource.java new file mode 100644 index 00000000000..915b9ee2c26 --- /dev/null +++ b/agent/src/com/cloud/agent/resource/computing/LibvirtComputingResource.java @@ -0,0 +1,3551 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + + +package com.cloud.agent.resource.computing; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.SortedMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.DomainInfo; +import org.libvirt.DomainInterfaceStats; +import org.libvirt.DomainSnapshot; +import org.libvirt.LibvirtException; +import org.libvirt.Network; +import org.libvirt.NodeInfo; +import org.libvirt.StoragePool; +import org.libvirt.StoragePoolInfo; +import org.libvirt.StorageVol; +import org.libvirt.StorageVolInfo; +import org.libvirt.StoragePoolInfo.StoragePoolState; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.AttachIsoCommand; +import com.cloud.agent.api.AttachVolumeAnswer; +import com.cloud.agent.api.AttachVolumeCommand; +import com.cloud.agent.api.BackupSnapshotAnswer; +import com.cloud.agent.api.BackupSnapshotCommand; +import com.cloud.agent.api.CheckHealthAnswer; +import com.cloud.agent.api.CheckHealthCommand; +import com.cloud.agent.api.CheckStateCommand; +import com.cloud.agent.api.CheckVirtualMachineAnswer; +import com.cloud.agent.api.CheckVirtualMachineCommand; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.CreatePrivateTemplateFromSnapshotCommand; +import com.cloud.agent.api.CreateVolumeFromSnapshotAnswer; +import com.cloud.agent.api.CreateVolumeFromSnapshotCommand; +import com.cloud.agent.api.DeleteSnapshotBackupAnswer; +import com.cloud.agent.api.DeleteSnapshotBackupCommand; +import com.cloud.agent.api.DeleteSnapshotsDirCommand; +import com.cloud.agent.api.DeleteStoragePoolCommand; +import com.cloud.agent.api.GetHostStatsAnswer; +import com.cloud.agent.api.GetHostStatsCommand; +import com.cloud.agent.api.GetStorageStatsAnswer; +import com.cloud.agent.api.GetStorageStatsCommand; +import com.cloud.agent.api.GetVmStatsAnswer; +import com.cloud.agent.api.GetVmStatsCommand; +import com.cloud.agent.api.GetVncPortAnswer; +import com.cloud.agent.api.GetVncPortCommand; +import com.cloud.agent.api.HostStatsEntry; +import com.cloud.agent.api.MaintainAnswer; +import com.cloud.agent.api.MaintainCommand; +import com.cloud.agent.api.ManageSnapshotAnswer; +import com.cloud.agent.api.ManageSnapshotCommand; +import com.cloud.agent.api.MigrateAnswer; +import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.MirrorCommand; +import com.cloud.agent.api.ModifySshKeysCommand; +import com.cloud.agent.api.ModifyStoragePoolAnswer; +import com.cloud.agent.api.ModifyStoragePoolCommand; +import com.cloud.agent.api.NetworkIngressRuleAnswer; +import com.cloud.agent.api.NetworkIngressRulesCmd; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.PingRoutingCommand; +import com.cloud.agent.api.PingTestCommand; +import com.cloud.agent.api.PrepareForMigrationAnswer; +import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.ReadyAnswer; +import com.cloud.agent.api.ReadyCommand; +import com.cloud.agent.api.RebootAnswer; +import com.cloud.agent.api.RebootCommand; +import com.cloud.agent.api.RebootRouterCommand; +import com.cloud.agent.api.StartAnswer; +import com.cloud.agent.api.StartCommand; +import com.cloud.agent.api.StartConsoleProxyAnswer; +import com.cloud.agent.api.StartConsoleProxyCommand; +import com.cloud.agent.api.StartRouterAnswer; +import com.cloud.agent.api.StartRouterCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.agent.api.StartupRoutingCommand; +import com.cloud.agent.api.StopAnswer; +import com.cloud.agent.api.StopCommand; +import com.cloud.agent.api.VmStatsEntry; +import com.cloud.agent.api.proxy.CheckConsoleProxyLoadCommand; +import com.cloud.agent.api.proxy.ConsoleProxyLoadAnswer; +import com.cloud.agent.api.proxy.WatchConsoleProxyLoadCommand; +import com.cloud.agent.api.routing.RoutingCommand; +import com.cloud.agent.api.storage.CreateAnswer; +import com.cloud.agent.api.storage.CreateCommand; +import com.cloud.agent.api.storage.CreatePrivateTemplateAnswer; +import com.cloud.agent.api.storage.CreatePrivateTemplateCommand; +import com.cloud.agent.api.storage.DownloadAnswer; +import com.cloud.agent.api.storage.PrimaryStorageDownloadCommand; +import com.cloud.agent.api.to.DiskCharacteristicsTO; +import com.cloud.agent.api.to.StoragePoolTO; +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.agent.resource.computing.LibvirtStoragePoolDef.poolType; +import com.cloud.agent.resource.computing.LibvirtStorageVolumeDef.volFormat; +import com.cloud.agent.resource.computing.LibvirtVMDef.consoleDef; +import com.cloud.agent.resource.computing.LibvirtVMDef.devicesDef; +import com.cloud.agent.resource.computing.LibvirtVMDef.diskDef; +import com.cloud.agent.resource.computing.LibvirtVMDef.featuresDef; +import com.cloud.agent.resource.computing.LibvirtVMDef.graphicDef; +import com.cloud.agent.resource.computing.LibvirtVMDef.guestDef; +import com.cloud.agent.resource.computing.LibvirtVMDef.guestResourceDef; +import com.cloud.agent.resource.computing.LibvirtVMDef.inputDef; +import com.cloud.agent.resource.computing.LibvirtVMDef.interfaceDef; +import com.cloud.agent.resource.computing.LibvirtVMDef.serialDef; +import com.cloud.agent.resource.computing.LibvirtVMDef.termPolicy; +import com.cloud.agent.resource.computing.LibvirtVMDef.interfaceDef.hostNicType; +import com.cloud.agent.resource.virtualnetwork.VirtualRoutingResource; +import com.cloud.exception.InternalErrorException; +import com.cloud.host.Host.Type; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.NetworkEnums.RouterPrivateIpStrategy; +import com.cloud.resource.ServerResource; +import com.cloud.resource.ServerResourceBase; +import com.cloud.storage.StorageLayer; +import com.cloud.storage.StoragePoolVO; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Volume.StorageResourceType; +import com.cloud.storage.Volume.VolumeType; +import com.cloud.storage.template.Processor; +import com.cloud.storage.template.QCOW2Processor; +import com.cloud.storage.template.TemplateInfo; +import com.cloud.storage.template.TemplateLocation; +import com.cloud.storage.template.Processor.FormatInfo; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; +import com.cloud.vm.ConsoleProxyVO; +import com.cloud.vm.DomainRouter; +import com.cloud.vm.State; +import com.cloud.vm.VirtualMachineName; + +/** + * LibvirtComputingResource execute requests on the computing/routing host using the libvirt API + * + * @config + * {@table + * || Param Name | Description | Values | Default || + * || hypervisor.type | type of local hypervisor | string | kvm || + * || hypervisor.uri | local hypervisor to connect to | URI | qemu:///system || + * || domr.arch | instruction set for domr template | string | i686 || + * || private.bridge.name | private bridge where the domrs have their private interface | string | vmops0 || + * || public.bridge.name | public bridge where the domrs have their public interface | string | br0 || + * || private.network.name | name of the network where the domrs have their private interface | string | vmops-private || + * || private.ipaddr.start | start of the range of private ip addresses for domrs | ip address | 192.168.166.128 || + * || private.ipaddr.end | end of the range of private ip addresses for domrs | ip address | start + 126 || + * || private.macaddr.start | start of the range of private mac addresses for domrs | mac address | 00:16:3e:77:e2:a0 || + * || private.macaddr.end | end of the range of private mac addresses for domrs | mac address | start + 126 || + * || pool | the parent of the storage pool hierarchy + * } + **/ +@Local(value={ServerResource.class}) +public class LibvirtComputingResource extends ServerResourceBase implements ServerResource { + private static final Logger s_logger = Logger.getLogger(LibvirtComputingResource.class); + + private String _createvnetPath; + private String _vnetcleanupPath; + private String _modifyVlanPath; + private String _versionstringpath; + private String _patchdomrPath; + private String _createvmPath; + private String _manageSnapshotPath; + private String _createTmplPath; + private String _host; + private String _dcId; + private String _pod; + private long _hvVersion; + private final String _SSHKEYSPATH = "/root/.ssh"; + private final String _SSHPRVKEYPATH = _SSHKEYSPATH + File.separator + "id_rsa.cloud"; + private final String _SSHPUBKEYPATH = _SSHKEYSPATH + File.separator + "id_rsa.pub.cloud"; + private final String _mountPoint = "/mnt"; + StorageLayer _storage; + + private static final class KeyValueInterpreter extends OutputInterpreter { + private final Map map = new HashMap(); + + @Override + public String interpret(BufferedReader reader) throws IOException { + String line = null; + int numLines=0; + while ((line = reader.readLine()) != null) { + String [] toks = line.trim().split("="); + if (toks.length < 2) s_logger.warn("Failed to parse Script output: " + line); + else map.put(toks[0].trim(), toks[1].trim()); + numLines++; + } + if (numLines == 0) { + s_logger.warn("KeyValueInterpreter: no output lines?"); + } + return null; + } + + public Map getKeyValues() { + return map; + } + } + + @Override + protected String getDefaultScriptsDir() { + return null; + } + + protected static MessageFormat domrXMLformat= new MessageFormat( "" + + " {1}" + + " {24}" + + " {2}" + + " 1" + + " " + + " hvm" + + " " + + " " + + " " + + " " + + " " + + " destroy" + + " restart" + + " destroy" + + " " + + " {23}" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + ""); + + protected static MessageFormat consoleProxyXMLformat= new MessageFormat( "" + + " {1}" + + " {2}" + + " {3}" + + " 1" + + " " + + " hvm" + + " " + + " " + + " " + + " " + + " " + + " destroy" + + " restart" + + " destroy" + + " " + + " {13}" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + ""); + + protected static MessageFormat vmXMLformat= new MessageFormat( "" + + " {1}" + + " {2}" + + " {3}" + + " {4}" + + " " + + " hvm" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " destroy" + + " restart" + + " destroy" + + " " + + " {6}" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + ""); + + protected static MessageFormat IsoXMLformat = new MessageFormat( + " " + + " " + + " " + + " " + + " "); + + protected static MessageFormat DiskXMLformat = new MessageFormat( + " " + + " " + + " " + + " "); + + protected static MessageFormat SnapshotXML = new MessageFormat( + " " + + " {0}" + + " " + + " {1}" + + " " + + " "); + + protected Connect _conn; + protected String _hypervisorType; + protected String _hypervisorURI; + protected String _hypervisorPath; + protected String _privNwName; + protected String _privBridgeName; + protected String _linkLocalBridgeName; + protected String _publicBridgeName; + protected String _privateBridgeIp; + protected String _domrArch; + protected String _domrKernel; + protected String _domrRamdisk; + protected String _pool; + private boolean _can_bridge_firewall; + private Pair _pifs; + private final Map _vmStats = new ConcurrentHashMap(); + + protected boolean _disconnected = true; + protected int _timeout; + protected int _stopTimeout; + protected static HashMap s_statesTable; + static { + s_statesTable = new HashMap(); + s_statesTable.put(DomainInfo.DomainState.VIR_DOMAIN_SHUTOFF, State.Stopped); + s_statesTable.put(DomainInfo.DomainState.VIR_DOMAIN_PAUSED, State.Running); + s_statesTable.put(DomainInfo.DomainState.VIR_DOMAIN_RUNNING, State.Running); + s_statesTable.put(DomainInfo.DomainState.VIR_DOMAIN_BLOCKED, State.Running); + s_statesTable.put(DomainInfo.DomainState.VIR_DOMAIN_NOSTATE, State.Unknown); + s_statesTable.put(DomainInfo.DomainState.VIR_DOMAIN_SHUTDOWN, State.Stopping); + } + + protected HashMap _vms = new HashMap(20); + protected List _vmsKilled = new ArrayList(); + + protected BitSet _domrIndex; + + private VirtualRoutingResource _virtRouterResource; + + private boolean _debug; + + private String _pingTestPath; + + private int _dom0MinMem; + + private enum defineOps { + UNDEFINE_VM, + DEFINE_VM + } + + private String getEndIpFromStartIp(String startIp, int numIps) { + String[] tokens = startIp.split("[.]"); + assert(tokens.length == 4); + int lastbyte = Integer.parseInt(tokens[3]); + lastbyte = lastbyte + numIps; + tokens[3] = Integer.toString(lastbyte); + StringBuilder end = new StringBuilder(15); + end.append(tokens[0]).append(".").append(tokens[1]).append(".").append(tokens[2]).append(".").append(tokens[3]); + return end.toString(); + } + + private Map getDeveloperProperties() throws ConfigurationException { + final File file = PropertiesUtil.findConfigFile("developer.properties"); + if (file == null) { + throw new ConfigurationException("Unable to find developer.properties."); + } + + s_logger.info("developer.properties found at " + file.getAbsolutePath()); + Properties properties = new Properties(); + try { + properties.load(new FileInputStream(file)); + + String startMac = (String)properties.get("private.macaddr.start"); + if (startMac == null) { + throw new ConfigurationException("Developers must specify start mac for private ip range"); + } + + String startIp = (String)properties.get("private.ipaddr.start"); + if (startIp == null) { + throw new ConfigurationException("Developers must specify start ip for private ip range"); + } + final Map params = PropertiesUtil.toMap(properties); + + String endIp = (String)properties.get("private.ipaddr.end"); + if (endIp == null) { + endIp = getEndIpFromStartIp(startIp, 16); + params.put("private.ipaddr.end", endIp); + } + return params; + } catch (final FileNotFoundException ex) { + throw new CloudRuntimeException("Cannot find the file: " + file.getAbsolutePath(), ex); + } catch (final IOException ex) { + throw new CloudRuntimeException("IOException in reading " + file.getAbsolutePath(), ex); + } + } + + protected String getDefaultNetworkScriptsDir() { + return "scripts/vm/network/vnet"; + } + + protected String getDefaultStorageScriptsDir() { + return "scripts/storage/qcow2"; + } + + @Override + public boolean configure(String name, Map params) + throws ConfigurationException { + boolean success = super.configure(name, params); + if (! success) + return false; + _virtRouterResource = new VirtualRoutingResource(); + + // Set the domr scripts directory + params.put("domr.scripts.dir", "scripts/network/domr/kvm"); + + success = _virtRouterResource.configure(name, params); + + String kvmScriptsDir = (String)params.get("kvm.scripts.dir"); + if (kvmScriptsDir == null) { + kvmScriptsDir = "scripts/vm/hypervisor/kvm"; + } + + String networkScriptsDir = (String)params.get("network.scripts.dir"); + if (networkScriptsDir == null) { + networkScriptsDir = getDefaultNetworkScriptsDir(); + } + + String storageScriptsDir = (String)params.get("storage.scripts.dir"); + if (storageScriptsDir == null) { + storageScriptsDir = getDefaultStorageScriptsDir(); + } + + if ( ! success) { + return false; + } + + _host = (String)params.get("host"); + if (_host == null) { + _host = "localhost"; + } + + + _dcId = (String) params.get("zone"); + if (_dcId == null) { + _dcId = "default"; + } + + _pod = (String) params.get("pod"); + if (_pod == null) { + _pod = "default"; + } + + _createvnetPath = Script.findScript(networkScriptsDir, "createvnet.sh"); + if(_createvnetPath == null) { + throw new ConfigurationException("Unable to find createvnet.sh"); + } + + _vnetcleanupPath = Script.findScript(networkScriptsDir, "vnetcleanup.sh"); + if(_vnetcleanupPath == null) { + throw new ConfigurationException("Unable to find createvnet.sh"); + } + + _modifyVlanPath = Script.findScript(networkScriptsDir, "modifyvlan.sh"); + if (_modifyVlanPath == null) { + throw new ConfigurationException("Unable to find modifyvlan.sh"); + } + + _versionstringpath = Script.findScript(kvmScriptsDir, "versions.sh"); + if (_versionstringpath == null) { + throw new ConfigurationException("Unable to find versions.sh"); + } + + _patchdomrPath = Script.findScript(kvmScriptsDir + "/patch/", "rundomrpre.sh"); + if (_patchdomrPath == null) { + throw new ConfigurationException("Unable to find rundomrpre.sh"); + } + + _createvmPath = Script.findScript(storageScriptsDir, "createvm.sh"); + if (_createvmPath == null) { + throw new ConfigurationException("Unable to find the createvm.sh"); + } + + _manageSnapshotPath = Script.findScript(storageScriptsDir, "managesnapshot.sh"); + if (_manageSnapshotPath == null) { + throw new ConfigurationException("Unable to find the managesnapshot.sh"); + } + + _createTmplPath = Script.findScript(storageScriptsDir, "createtmplt.sh"); + if (_createTmplPath == null) { + throw new ConfigurationException("Unable to find the createtmplt.sh"); + } + + String value = (String)params.get("developer"); + boolean isDeveloper = Boolean.parseBoolean(value); + + if (isDeveloper) { + params.putAll(getDeveloperProperties()); + } + + _pool = (String) params.get("pool"); + if (_pool == null) { + _pool = "/root"; + } + + + String instance = (String)params.get("instance"); + + _hypervisorType = (String)params.get("hypervisor.type"); + if (_hypervisorType == null) { + _hypervisorType = "kvm"; + } + + _hypervisorURI = (String)params.get("hypervisor.uri"); + if (_hypervisorURI == null) { + _hypervisorURI = "qemu:///system"; + } + String startMac = (String)params.get("private.macaddr.start"); + if (startMac == null) { + startMac = "00:16:3e:77:e2:a0"; + } + + String startIp = (String)params.get("private.ipaddr.start"); + if (startIp == null) { + startIp = "192.168.166.128"; + } + + _pingTestPath = Script.findScript(kvmScriptsDir, "pingtest.sh"); + if (_pingTestPath == null) { + throw new ConfigurationException("Unable to find the pingtest.sh"); + } + + _linkLocalBridgeName = (String)params.get("private.bridge.name"); + if (_linkLocalBridgeName == null) { + if (isDeveloper) { + _linkLocalBridgeName = "cloud-" + instance + "-0"; + } else { + _linkLocalBridgeName = "cloud0"; + } + } + + _publicBridgeName = (String)params.get("public.network.device"); + if (_publicBridgeName == null) { + _publicBridgeName = "cloudbr0"; + } + + _privBridgeName = (String)params.get("private.network.device"); + if (_privBridgeName == null) { + _privBridgeName = "cloudbr1"; + } + + _privNwName = (String)params.get("private.network.name"); + if (_privNwName == null) { + if (isDeveloper) { + _privNwName = "cloud-" + instance + "-private"; + } else { + _privNwName = "cloud-private"; + } + } + + value = (String)params.get("scripts.timeout"); + _timeout = NumbersUtil.parseInt(value, 120) * 1000; + + value = (String)params.get("stop.script.timeout"); + _stopTimeout = NumbersUtil.parseInt(value, 120) * 1000; + + + _domrArch = (String)params.get("domr.arch"); + if (_domrArch == null ) { + _domrArch = "i686"; + } else if (!"i686".equalsIgnoreCase(_domrArch) && !"x86_64".equalsIgnoreCase(_domrArch)) { + throw new ConfigurationException("Invalid architecture (domr.arch) -- needs to be i686 or x86_64"); + } + + _domrKernel = (String)params.get("domr.kernel"); + if (_domrKernel == null ) { + _domrKernel = new File("/var/lib/libvirt/images/vmops-domr-kernel").getAbsolutePath(); + } + + _domrRamdisk = (String)params.get("domr.ramdisk"); + if (_domrRamdisk == null ) { + _domrRamdisk = new File("/var/lib/libvirt/images/vmops-domr-initramfs").getAbsolutePath(); + } + + + value = (String)params.get("host.reserved.mem.mb"); + _dom0MinMem = NumbersUtil.parseInt(value, 0)*1024*1024; + + + value = (String)params.get("debug.mode"); + _debug = Boolean.parseBoolean(value); + + try{ + _conn = new Connect(_hypervisorURI, false); + } catch (LibvirtException e){ + throw new ConfigurationException("Unable to connect to hypervisor: " + e.getMessage()); + } + + /* Does node support HVM guest? If not, exit*/ + if (!IsHVMEnabled()) { + throw new ConfigurationException("NO HVM support on this machine, pls make sure: " + + "1. VT/SVM is supported by your CPU, or is enabled in BIOS. " + + "2. kvm modules is installed"); + } + + _hypervisorPath = getHypervisorPath(); + try { + _hvVersion = _conn.getVersion(); + _hvVersion = (_hvVersion % 1000000) / 1000; + } catch (LibvirtException e) { + + } + + try { + Class clazz = Class.forName("com.cloud.storage.JavaStorageLayer"); + _storage = (StorageLayer)ComponentLocator.inject(clazz); + _storage.configure("StorageLayer", params); + } catch (ClassNotFoundException e) { + throw new ConfigurationException("Unable to find class " + "com.cloud.storage.JavaStorageLayer"); + } + + //_can_bridge_firewall = can_bridge_firewall(); + + Network vmopsNw = null; + try { + vmopsNw = _conn.networkLookupByName(_privNwName); + } catch (LibvirtException lve){ + + } + deletExitingLinkLocalRoutTable(_linkLocalBridgeName); + if (vmopsNw == null) { + try { + /*vmopsNw = conn.networkCreateXML("" + + " vmops-private"+ + " "+ + " "+ + " "+ + " + + " "+ + " "+ + "");*/ + + _virtRouterResource.cleanupPrivateNetwork(_privNwName, _linkLocalBridgeName); + LibvirtNetworkDef networkDef = new LibvirtNetworkDef(_privNwName, null, null); + networkDef.defLocalNetwork(_linkLocalBridgeName, false, 0, NetUtils.getLinkLocalGateway(), NetUtils.getLinkLocalNetMask()); + + String nwXML = networkDef.toString(); + s_logger.debug(nwXML); + vmopsNw = _conn.networkCreateXML(nwXML); + + } catch (LibvirtException lve) { + throw new ConfigurationException("Unable to define private network " + lve.getMessage()); + } + } else { + s_logger.info("Found private network " + _privNwName + " already defined"); + } + + _pifs = getPifs(); + if (_pifs.first() == null) { + s_logger.debug("Failed to get private nic name"); + throw new ConfigurationException("Failed to get private nic name"); + } + + if (_pifs.second() == null) { + s_logger.debug("Failed to get public nic name"); + throw new ConfigurationException("Failed to get public nic name"); + } + s_logger.debug("Found pif: " + _pifs.first() + " on " + _privBridgeName + ", pif: " + _pifs.second() + " on " + _publicBridgeName); + return true; + } + + private Pair getPifs() { + /*get pifs from bridge*/ + String pubPif = null; + String privPif = null; + if (_publicBridgeName != null) { + pubPif = Script.runSimpleBashScript("ls /sys/class/net/" + _publicBridgeName + "/brif/ |egrep eth[0-9]+"); + } + if (_privBridgeName != null) { + privPif = Script.runSimpleBashScript("ls /sys/class/net/" + _privBridgeName + "/brif/ |egrep eth[0-9]+"); + } + return new Pair(privPif, pubPif); + } + private String getVnetId(String vnetId) { + return vnetId; + } + + private void patchSystemVm(String cmdLine, String dataDiskPath, String vmName) throws InternalErrorException { + String result; + final Script command = new Script(_patchdomrPath, _timeout, s_logger); + command.add("-l", vmName); + command.add("-t", "all"); + command.add("-d", dataDiskPath); + command.add("-p", cmdLine.replaceAll(" ", ",")); + result = command.execute(); + if (result != null) { + throw new InternalErrorException(result); + } + } + + private void disableBridgeForwardding(String vnetBridge) { + //TODO: workaround for KVM on ubuntu, disable bridge forward table + Script disableFD = new Script("/bin/bash", _timeout); + disableFD.add("-c"); + disableFD.add("brctl setfd " + vnetBridge + " 0; brctl setageing " + vnetBridge + " 0"); + disableFD.execute(); + } + + boolean isDirectAttachedNetwork(String type) { + if ("untagged".equalsIgnoreCase(type)) + return true; + else + return false; + } + + protected synchronized String startDomainRouter(StartRouterCommand cmd) { + DomainRouter router = cmd.getRouter(); + List nics = null; + try { + nics = createRouterVMNetworks(cmd); + + List disks = createSystemVMDisk(cmd.getVolumes()); + + String dataDiskPath = null; + for (diskDef disk : disks) { + if (disk.getDiskLabel().equalsIgnoreCase("hdb")) { + dataDiskPath = disk.getDiskPath(); + } + } + + String vmName = cmd.getVmName(); + patchSystemVm(cmd.getBootArgs(), dataDiskPath, vmName); + + String uuid = UUID.nameUUIDFromBytes(vmName.getBytes()).toString(); + String domXML = defineVMXML(cmd.getVmName(), uuid, router.getRamSize(), 1, _domrArch, nics, disks, router.getVncPassword(), "Fedora 12"); + + s_logger.debug(domXML); + + startDomain(vmName, domXML); + + for (interfaceDef nic : nics) { + if (nic.getHostNetType() == hostNicType.VNET) { + disableBridgeForwardding(nic.getBrName()); + } + } + + /*if (isDirectAttachedNetwork(router.getVlanId())) + default_network_rules_for_systemvm(vmName);*/ + } catch (LibvirtException e) { + if (nics != null) { + cleanupVMNetworks(nics); + } + s_logger.debug("Failed to start domr: " + e.toString()); + return e.toString(); + }catch (InternalErrorException e) { + if (nics != null) { + cleanupVMNetworks(nics); + } + s_logger.debug("Failed to start domr: " + e.toString()); + return e.toString(); + } + return null; + } + + protected synchronized String startConsoleProxy(StartConsoleProxyCommand cmd) { + ConsoleProxyVO console = cmd.getProxy(); + List nics = null; + try { + nics = createConsoleVMNetworks(cmd); + + List disks = createSystemVMDisk(cmd.getVolumes()); + + String dataDiskPath = null; + for (diskDef disk : disks) { + if (disk.getDiskLabel().equalsIgnoreCase("hdb")) { + dataDiskPath = disk.getDiskPath(); + } + } + + String vmName = cmd.getVmName(); + patchSystemVm(cmd.getBootArgs(), dataDiskPath, vmName); + + String uuid = UUID.nameUUIDFromBytes(vmName.getBytes()).toString(); + String domXML = defineVMXML(cmd.getVmName(), uuid, console.getRamSize(), 1, _domrArch, nics, disks, console.getVncPassword(), "Fedora 12"); + + s_logger.debug(domXML); + + startDomain(vmName, domXML); + } catch (LibvirtException e) { + s_logger.debug("Failed to start domr: " + e.toString()); + return e.toString(); + }catch (InternalErrorException e) { + s_logger.debug("Failed to start domr: " + e.toString()); + return e.toString(); + } + return null; + } + + private String defineVMXML(String vmName, String uuid, int memSize, int cpus, String arch, List nics, List disks, String vncPaswd, String guestOSType) { + LibvirtVMDef vm = new LibvirtVMDef(); + vm.setHvsType(_hypervisorType); + vm.setDomainName(vmName); + vm.setDomUUID(uuid); + vm.setDomDescription(guestOSType); + + guestDef guest = new guestDef(); + guest.setGuestType(guestDef.guestType.KVM); + guest.setGuestArch(arch); + guest.setMachineType("pc"); + guest.setBootOrder(guestDef.bootOrder.CDROM); + guest.setBootOrder(guestDef.bootOrder.HARDISK); + + vm.addComp(guest); + + guestResourceDef grd = new guestResourceDef(); + grd.setMemorySize(memSize*1024); + grd.setVcpuNum(cpus); + vm.addComp(grd); + + featuresDef features = new featuresDef(); + features.addFeatures("pae"); + features.addFeatures("apic"); + features.addFeatures("acpi"); + vm.addComp(features); + + termPolicy term = new termPolicy(); + term.setCrashPolicy("destroy"); + term.setPowerOffPolicy("destroy"); + term.setRebootPolicy("restart"); + vm.addComp(term); + + devicesDef devices = new devicesDef(); + devices.setEmulatorPath(_hypervisorPath); + + for (interfaceDef nic : nics) { + devices.addDevice(nic); + } + + for (diskDef disk : disks) { + if (!disk.isAttachDeferred()) + devices.addDevice(disk); + } + + serialDef serial = new serialDef("pty", null, (short)0); + devices.addDevice(serial); + + consoleDef console = new consoleDef("pty", null, null, (short)0); + devices.addDevice(console); + + graphicDef grap = new graphicDef("vnc", (short)0, true, null, null, null); + devices.addDevice(grap); + + inputDef input = new inputDef("tablet", "usb"); + devices.addDevice(input); + + vm.addComp(devices); + + String domXML = vm.toString(); + + s_logger.debug(domXML); + return domXML; + } + protected String startDomain(String vmName, String domainXML) throws LibvirtException{ + /*No duplicated vm, we will success, or failed*/ + boolean failed =false; + Domain dm = null; + try { + dm = _conn.domainDefineXML(domainXML); + } catch (final LibvirtException e) { + /*Duplicated defined vm*/ + s_logger.warn("Failed to define domain " + vmName + ": " + e.getMessage()); + failed = true; + } finally { + try { + if (dm != null) + dm.free(); + } catch (final LibvirtException e) { + + } + } + + /*If failed, undefine the vm*/ + Domain dmOld = null; + Domain dmNew = null; + try { + if (failed) { + dmOld = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + dmOld.undefine(); + dmNew = _conn.domainDefineXML(domainXML); + } + } catch (final LibvirtException e) { + s_logger.warn("Failed to define domain (second time) " + vmName + ": " + e.getMessage()); + throw e; + } finally { + try { + if (dmOld != null) + dmOld.free(); + if (dmNew != null) + dmNew.free(); + } catch (final LibvirtException e) { + + } + } + + /*Start the VM*/ + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + dm.create(); + } catch (LibvirtException e) { + s_logger.warn("Failed to start domain: " + vmName + ": " + e.getMessage()); + throw e; + } finally { + try { + if (dm != null) + dm.free(); + } catch (final LibvirtException e) { + + } + } + return null; + } + + @Override + public boolean stop() { + if (_conn != null) { + try { + _conn.close(); + } catch (LibvirtException e) { + } + _conn = null; + } + return true; + } + + public static void main(String[] args) { + s_logger.addAppender(new org.apache.log4j.ConsoleAppender(new org.apache.log4j.PatternLayout(), "System.out")); + LibvirtComputingResource test = new LibvirtComputingResource(); + Map params = new HashMap(); + try { + test.configure("test", params); + } catch (ConfigurationException e) { + System.out.println(e.getMessage()); + e.printStackTrace(); + } + String result = null; + //String result = test.startDomainRouter("domr1", "/var/lib/images/centos.5-4.x86-64/centos-small.img", 128, "0064", "02:00:30:00:01:01", "00:16:3e:77:e2:a1", "02:00:30:00:64:01"); + boolean created = (result==null); + s_logger.info("Domain " + (created?" ":" not ") + " created"); + + s_logger.info("Rule " + (created?" ":" not ") + " created"); + test.stop(); + } + + @Override + public Answer executeRequest(Command cmd) { + + try { + if (cmd instanceof StartCommand) { + return execute((StartCommand)cmd); + } else if (cmd instanceof StopCommand) { + return execute((StopCommand)cmd); + } else if (cmd instanceof GetVmStatsCommand) { + return execute((GetVmStatsCommand)cmd); + } else if (cmd instanceof RebootRouterCommand) { + return execute((RebootRouterCommand)cmd); + } else if (cmd instanceof RebootCommand) { + return execute((RebootCommand)cmd); + } else if (cmd instanceof GetHostStatsCommand) { + return execute((GetHostStatsCommand)cmd); + } else if (cmd instanceof CheckStateCommand) { + return executeRequest(cmd); + } else if (cmd instanceof MirrorCommand) { + return executeRequest(cmd); + } else if (cmd instanceof CheckHealthCommand) { + return execute((CheckHealthCommand)cmd); + } else if (cmd instanceof PrepareForMigrationCommand) { + return execute((PrepareForMigrationCommand)cmd); + } else if (cmd instanceof MigrateCommand) { + return execute((MigrateCommand)cmd); + } else if (cmd instanceof PingTestCommand) { + return execute((PingTestCommand)cmd); + } else if (cmd instanceof CheckVirtualMachineCommand) { + return execute((CheckVirtualMachineCommand)cmd); + } else if (cmd instanceof ReadyCommand) { + return execute((ReadyCommand)cmd); + } else if (cmd instanceof StartRouterCommand) { + return execute((StartRouterCommand)cmd); + } else if(cmd instanceof StartConsoleProxyCommand) { + return execute((StartConsoleProxyCommand)cmd); + } else if (cmd instanceof AttachIsoCommand) { + return execute((AttachIsoCommand) cmd); + } else if (cmd instanceof AttachVolumeCommand) { + return execute((AttachVolumeCommand) cmd); + } else if (cmd instanceof StopCommand) { + return execute((StopCommand)cmd); + } else if (cmd instanceof CheckConsoleProxyLoadCommand) { + return execute((CheckConsoleProxyLoadCommand)cmd); + } else if (cmd instanceof WatchConsoleProxyLoadCommand) { + return execute((WatchConsoleProxyLoadCommand)cmd); + } else if (cmd instanceof GetVncPortCommand) { + return execute((GetVncPortCommand)cmd); + } else if (cmd instanceof ModifySshKeysCommand) { + return execute((ModifySshKeysCommand)cmd); + } else if (cmd instanceof MaintainCommand) { + return execute((MaintainCommand) cmd); + } else if (cmd instanceof CreateCommand) { + return execute((CreateCommand) cmd); + } else if (cmd instanceof PrimaryStorageDownloadCommand) { + return execute((PrimaryStorageDownloadCommand) cmd); + } else if (cmd instanceof CreatePrivateTemplateCommand) { + return execute((CreatePrivateTemplateCommand) cmd); + } else if (cmd instanceof GetStorageStatsCommand) { + return execute((GetStorageStatsCommand) cmd); + } else if (cmd instanceof ManageSnapshotCommand) { + return execute((ManageSnapshotCommand) cmd); + } else if (cmd instanceof BackupSnapshotCommand) { + return execute((BackupSnapshotCommand) cmd); + } else if (cmd instanceof DeleteSnapshotBackupCommand) { + return execute((DeleteSnapshotBackupCommand) cmd); + } else if (cmd instanceof DeleteSnapshotsDirCommand) { + return execute((DeleteSnapshotsDirCommand) cmd); + } else if (cmd instanceof CreateVolumeFromSnapshotCommand) { + return execute((CreateVolumeFromSnapshotCommand) cmd); + } else if (cmd instanceof CreatePrivateTemplateFromSnapshotCommand) { + return execute((CreatePrivateTemplateFromSnapshotCommand) cmd); + } else if (cmd instanceof ModifyStoragePoolCommand) { + return execute((ModifyStoragePoolCommand) cmd); + } else if (cmd instanceof NetworkIngressRulesCmd) { + return execute((NetworkIngressRulesCmd) cmd); + } else if (cmd instanceof DeleteStoragePoolCommand) { + return execute((DeleteStoragePoolCommand) cmd); + } else if (cmd instanceof RoutingCommand) { + return _virtRouterResource.executeRequest(cmd); + } else { + s_logger.warn("Unsupported command "); + return Answer.createUnsupportedCommandAnswer(cmd); + } + } catch (final IllegalArgumentException e) { + return new Answer(cmd, false, e.getMessage()); + } + } + + + protected Answer execute(DeleteStoragePoolCommand cmd) { + try { + StoragePool pool = _conn.storagePoolLookupByUUIDString(cmd.getPool().getUuid()); + pool.destroy(); + pool.undefine(); + return new Answer(cmd); + } catch (LibvirtException e) { + return new Answer(cmd, false, e.toString()); + } + } + + protected StorageResourceType getStorageResourceType() { + return StorageResourceType.STORAGE_POOL; + } + + protected Answer execute(CreateCommand cmd) { + StoragePoolTO pool = cmd.getPool(); + DiskCharacteristicsTO dskch = cmd.getDiskCharacteristics(); + StorageVol tmplVol = null; + StoragePool primaryPool = null; + StorageVol vol = null; + long disksize; + try { + primaryPool = _conn.storagePoolLookupByUUIDString(pool.getUuid()); + if (primaryPool == null) { + String result = "Failed to get primary pool"; + s_logger.debug(result); + return new CreateAnswer(cmd, result); + } + + if (cmd.getTemplateUrl() != null) { + tmplVol = getVolume(primaryPool, cmd.getTemplateUrl()); + if (tmplVol == null) { + String result = "Failed to get tmpl vol"; + s_logger.debug(result); + return new CreateAnswer(cmd, result); + } + + LibvirtStorageVolumeDef volDef = new LibvirtStorageVolumeDef(UUID.randomUUID().toString(), tmplVol.getInfo().capacity, volFormat.QCOW2, tmplVol.getPath(), volFormat.QCOW2); + s_logger.debug(volDef.toString()); + vol = primaryPool.storageVolCreateXML(volDef.toString(), 0); + if (vol == null) { + return new Answer(cmd, false, " Can't create storage volume on storage pool"); + } + disksize = tmplVol.getInfo().capacity; + } else { + disksize = dskch.getSize(); + LibvirtStorageVolumeDef volDef = new LibvirtStorageVolumeDef(UUID.randomUUID().toString(), disksize, volFormat.QCOW2, null, null); + s_logger.debug(volDef.toString()); + vol = primaryPool.storageVolCreateXML(volDef.toString(), 0); + + } + VolumeTO volume = new VolumeTO(cmd.getVolumeId(), dskch.getType(), getStorageResourceType(), pool.getType(), pool.getPath(), vol.getName(),vol.getKey(), disksize); + return new CreateAnswer(cmd, volume); + } catch (LibvirtException e) { + + s_logger.debug("Failed to create volume: " + e.toString()); + return new CreateAnswer(cmd, e); + } finally { + try { + if (vol != null) { + vol.free(); + } + if (tmplVol != null) { + tmplVol.free(); + } + if (primaryPool != null) { + primaryPool.free(); + } + } catch (LibvirtException e) { + + } + } + } + protected ManageSnapshotAnswer execute(final ManageSnapshotCommand cmd) { + String snapshotName = cmd.getSnapshotName(); + String VolPath = cmd.getVolumePath(); + try { + StorageVol vol = getVolume(VolPath); + if (vol == null) { + return new ManageSnapshotAnswer(cmd, false, null); + } + Domain vm = getDomain(cmd.getVmName()); + String vmUuid = vm.getUUIDString(); + Object[] args = new Object[] {snapshotName, vmUuid}; + String snapshot = SnapshotXML.format(args); + s_logger.debug(snapshot); + if (cmd.getCommandSwitch().equalsIgnoreCase(ManageSnapshotCommand.CREATE_SNAPSHOT)) { + vm.snapshotCreateXML(snapshot); + } else { + DomainSnapshot snap = vm.snapshotLookupByName(snapshotName); + snap.delete(0); + } + } catch (LibvirtException e) { + s_logger.debug("Failed to manage snapshot: " + e.toString()); + return new ManageSnapshotAnswer(cmd, false, "Failed to manage snapshot: " + e.toString()); + } + return new ManageSnapshotAnswer(cmd, cmd.getSnapshotId(), cmd.getVolumePath(), true, null); + } + + protected BackupSnapshotAnswer execute(final BackupSnapshotCommand cmd) { + Long dcId = cmd.getDataCenterId(); + Long accountId = cmd.getAccountId(); + Long volumeId = cmd.getVolumeId(); + String secondaryStoragePoolURL = cmd.getSecondaryStoragePoolURL(); + String snapshotName = cmd.getSnapshotName(); + String snapshotPath = cmd.getSnapshotUuid(); + String snapshotDestPath = null; + + try { + StoragePool secondaryStoragePool = getNfsSPbyURI(_conn, new URI(secondaryStoragePoolURL)); + String ssPmountPath = _mountPoint + File.separator + secondaryStoragePool.getUUIDString(); + snapshotDestPath = ssPmountPath + File.separator + dcId + File.separator + "snapshots" + File.separator + accountId + File.separator + volumeId; + final Script command = new Script(_manageSnapshotPath, _timeout, s_logger); + command.add("-b", snapshotPath); + command.add("-n", snapshotName); + command.add("-p", snapshotDestPath); + String result = command.execute(); + if (result != null) { + s_logger.debug("Failed to backup snaptshot: " + result); + return new BackupSnapshotAnswer(cmd, false, result, null); + } + /*Delete the snapshot on primary*/ + Domain vm = getDomain(cmd.getVmName()); + String vmUuid = vm.getUUIDString(); + Object[] args = new Object[] {snapshotName, vmUuid}; + String snapshot = SnapshotXML.format(args); + s_logger.debug(snapshot); + DomainSnapshot snap = vm.snapshotLookupByName(snapshotName); + snap.delete(0); + } catch (LibvirtException e) { + return new BackupSnapshotAnswer(cmd, false, e.toString(), null); + } catch (URISyntaxException e) { + return new BackupSnapshotAnswer(cmd, false, e.toString(), null); + } + return new BackupSnapshotAnswer(cmd, true, null, snapshotDestPath + File.separator + snapshotName); + } + + protected DeleteSnapshotBackupAnswer execute(final DeleteSnapshotBackupCommand cmd) { + Long dcId = cmd.getDataCenterId(); + Long accountId = cmd.getAccountId(); + Long volumeId = cmd.getVolumeId(); + try { + StoragePool secondaryStoragePool = getNfsSPbyURI(_conn, new URI(cmd.getSecondaryStoragePoolURL())); + String ssPmountPath = _mountPoint + File.separator + secondaryStoragePool.getUUIDString(); + String snapshotDestPath = ssPmountPath + File.separator + dcId + File.separator + "snapshots" + File.separator + accountId + File.separator + volumeId; + + final Script command = new Script(_manageSnapshotPath, _timeout, s_logger); + command.add("-d", snapshotDestPath); + command.add("-n", cmd.getSnapshotName()); + + command.execute(); + } catch (LibvirtException e) { + return new DeleteSnapshotBackupAnswer(cmd, false, e.toString()); + } catch (URISyntaxException e) { + return new DeleteSnapshotBackupAnswer(cmd, false, e.toString()); + } + return new DeleteSnapshotBackupAnswer(cmd, true, null); + } + + protected Answer execute(DeleteSnapshotsDirCommand cmd) { + Long dcId = cmd.getDataCenterId(); + Long accountId = cmd.getAccountId(); + Long volumeId = cmd.getVolumeId(); + try { + StoragePool secondaryStoragePool = getNfsSPbyURI(_conn, new URI(cmd.getSecondaryStoragePoolURL())); + String ssPmountPath = _mountPoint + File.separator + secondaryStoragePool.getUUIDString(); + String snapshotDestPath = ssPmountPath + File.separator + dcId + File.separator + "snapshots" + File.separator + accountId + File.separator + volumeId; + + final Script command = new Script(_manageSnapshotPath, _timeout, s_logger); + command.add("-d", snapshotDestPath); + command.add("-n", cmd.getSnapshotName()); + command.execute(); + } catch (LibvirtException e) { + return new Answer(cmd, false, e.toString()); + } catch (URISyntaxException e) { + return new Answer(cmd, false, e.toString()); + } + return new Answer(cmd, true, null); + } + + protected CreateVolumeFromSnapshotAnswer execute(final CreateVolumeFromSnapshotCommand cmd) { + String snapshotPath = cmd.getSnapshotUuid(); + String primaryUuid = cmd.getPrimaryStoragePoolNameLabel(); + String primaryPath = _mountPoint + File.separator + primaryUuid; + String volUuid = UUID.randomUUID().toString(); + String volPath = primaryPath + File.separator + volUuid; + String result = Script.runSimpleBashScript("cp " + snapshotPath + " " + volPath); + if (result != null) { + return new CreateVolumeFromSnapshotAnswer(cmd, false, result, null); + } + return new CreateVolumeFromSnapshotAnswer(cmd, true, "", volPath); + } + + protected CreatePrivateTemplateAnswer execute(final CreatePrivateTemplateFromSnapshotCommand cmd) { + String orignalTmplPath = cmd.getOrigTemplateInstallPath(); + String templateFolder = cmd.getAccountId() + File.separator + cmd.getNewTemplateId(); + String templateInstallFolder = "template/tmpl/" + templateFolder; + String snapshotPath = cmd.getSnapshotUuid(); + String tmplName = UUID.randomUUID().toString(); + String tmplFileName = tmplName + ".qcow2"; + StoragePool secondaryPool; + try { + secondaryPool = getNfsSPbyURI(_conn, new URI(cmd.getSecondaryStoragePoolURL())); + /*TODO: assuming all the storage pools mounted under _mountPoint, the mount point should be got from pool.dumpxml*/ + String templatePath = _mountPoint + File.separator + secondaryPool.getUUIDString() + File.separator + templateInstallFolder; + String tmplPath = templateInstallFolder + File.separator + tmplFileName; + Script command = new Script(_createTmplPath, _timeout, s_logger); + command.add("-t", templatePath); + command.add("-n", tmplFileName); + command.add("-f", snapshotPath); + String result = command.execute(); + + Map params = new HashMap(); + params.put(StorageLayer.InstanceConfigKey, _storage); + Processor qcow2Processor = new QCOW2Processor(); + qcow2Processor.configure("QCOW2 Processor", params); + FormatInfo info = qcow2Processor.process(templatePath, null, tmplName); + + TemplateLocation loc = new TemplateLocation(_storage, templatePath); + loc.create(1, true, tmplName); + loc.addFormat(info); + loc.save(); + + return new CreatePrivateTemplateAnswer(cmd, true, "", tmplPath, info.virtualSize, tmplName, info.format); + } catch (LibvirtException e) { + return new CreatePrivateTemplateAnswer(cmd, false, e.getMessage(), null, 0, null, null); + } catch (URISyntaxException e) { + return new CreatePrivateTemplateAnswer(cmd, false, e.getMessage(), null, 0, null, null); + } catch (ConfigurationException e) { + return new CreatePrivateTemplateAnswer(cmd, false, e.getMessage(), null, 0, null, null); + } catch (InternalErrorException e) { + return new CreatePrivateTemplateAnswer(cmd, false, e.getMessage(), null, 0, null, null); + } catch (IOException e) { + return new CreatePrivateTemplateAnswer(cmd, false, e.getMessage(), null, 0, null, null); + } + } + + + protected GetStorageStatsAnswer execute(final GetStorageStatsCommand cmd) { + StoragePool sp = null; + StoragePoolInfo spi = null; + try { + sp = _conn.storagePoolLookupByUUIDString(cmd.getStorageId()); + spi = sp.getInfo(); + return new GetStorageStatsAnswer(cmd, spi.capacity, spi.allocation); + } catch (LibvirtException e) { + return new GetStorageStatsAnswer(cmd, e.toString()); + } + } + protected CreatePrivateTemplateAnswer execute(CreatePrivateTemplateCommand cmd) { + String secondaryStorageURL = cmd.getSecondaryStorageURL(); + String snapshotUUID = cmd.getSnapshotPath(); + + StoragePool secondaryStorage = null; + StoragePool privateTemplStorage = null; + StorageVol privateTemplateVol = null; + StorageVol snapshotVol = null; + try { + String templateFolder = cmd.getAccountId() + File.separator + cmd.getTemplateId() + File.separator; + String templateInstallFolder = "/template/tmpl/" + templateFolder; + + secondaryStorage = getNfsSPbyURI(_conn, new URI(secondaryStorageURL)); + /*TODO: assuming all the storage pools mounted under _mountPoint, the mount point should be got from pool.dumpxml*/ + String mountPath = _mountPoint + File.separator + secondaryStorage.getUUIDString() + templateInstallFolder; + File mpfile = new File(mountPath); + if (!mpfile.exists()) { + mpfile.mkdir(); + } + + // Create a SR for the secondary storage installation folder + privateTemplStorage = getNfsSPbyURI(_conn, new URI(secondaryStorageURL + templateInstallFolder)); + snapshotVol = getVolume(snapshotUUID); + + LibvirtStorageVolumeDef vol = new LibvirtStorageVolumeDef(UUID.randomUUID().toString(), snapshotVol.getInfo().capacity, volFormat.QCOW2, null, null); + s_logger.debug(vol.toString()); + privateTemplateVol = copyVolume(privateTemplStorage, vol, snapshotVol); + + return new CreatePrivateTemplateAnswer(cmd, + true, + null, + templateInstallFolder + privateTemplateVol.getName(), + privateTemplateVol.getInfo().capacity/1024*1024, /*in Mega unit*/ + privateTemplateVol.getName(), + ImageFormat.QCOW2); + } catch (URISyntaxException e) { + return new CreatePrivateTemplateAnswer(cmd, + false, + e.toString(), + null, + 0, + null, + null); + } catch (LibvirtException e) { + s_logger.debug("Failed to get secondary storage pool: " + e.toString()); + return new CreatePrivateTemplateAnswer(cmd, + false, + e.toString(), + null, + 0, + null, + null); + } + } + + private StoragePool getNfsSPbyURI(Connect conn, URI uri) throws LibvirtException { + String sourcePath = uri.getPath(); + sourcePath = sourcePath.replace("//", "/"); + String sourceHost = uri.getHost(); + String uuid = UUID.nameUUIDFromBytes(new String(sourceHost + sourcePath).getBytes()).toString(); + String targetPath = _mountPoint + File.separator + uuid; + StoragePool sp = null; + try { + sp = conn.storagePoolLookupByUUIDString(uuid); + } catch (LibvirtException e) { + } + + if (sp == null) { + try { + File tpFile = new File(targetPath); + if (!tpFile.exists()) { + tpFile.mkdir(); + } + LibvirtStoragePoolDef spd = new LibvirtStoragePoolDef(poolType.NFS, uuid, uuid, + sourceHost, sourcePath, targetPath); + s_logger.debug(spd.toString()); + sp = conn.storagePoolDefineXML(spd.toString(), 0); + if (sp == null) { + s_logger.debug("Failed to define storage pool"); + return null; + } + sp.create(0); + + return sp; + } catch (LibvirtException e) { + try { + if (sp != null) { + sp.undefine(); + sp.free(); + } + } catch (LibvirtException l) { + + } + throw e; + } + } else { + StoragePoolInfo spi = sp.getInfo(); + if (spi.state != StoragePoolState.VIR_STORAGE_POOL_RUNNING) { + sp.create(0); + } + return sp; + } + } + protected Answer execute(final PrimaryStorageDownloadCommand cmd) { + String tmplturl = cmd.getUrl(); + int index = tmplturl.lastIndexOf("/"); + String mountpoint = tmplturl.substring(0, index); + String tmpltname = null; + if (index < tmplturl.length() - 1) + tmpltname = tmplturl.substring(index + 1); + + StoragePool secondaryPool = null; + StoragePool primaryPool = null; + StorageVol tmplVol = null; + StorageVol primaryVol = null; + String result; + try { + secondaryPool = getNfsSPbyURI(_conn, new URI(mountpoint)); + if (secondaryPool == null) { + return new Answer(cmd, false, " Failed to create storage pool"); + } + tmplVol = getVolume(secondaryPool, getPathOfStoragePool(secondaryPool) + tmpltname); + if (tmplVol == null) { + return new Answer(cmd, false, " Can't find volume"); + } + primaryPool = _conn.storagePoolLookupByUUIDString(cmd.getPoolUuid()); + if (primaryPool == null) { + return new Answer(cmd, false, " Can't find primary storage pool"); + } + LibvirtStorageVolumeDef vol = new LibvirtStorageVolumeDef(UUID.randomUUID().toString(), tmplVol.getInfo().capacity, volFormat.QCOW2, null, null); + s_logger.debug(vol.toString()); + primaryVol = copyVolume(primaryPool, vol, tmplVol); + if (primaryVol == null) { + return new Answer(cmd, false, " Can't create storage volume on storage pool"); + } + + StorageVolInfo priVolInfo = primaryVol.getInfo(); + DownloadAnswer answer = new DownloadAnswer(null, + 100, + cmd, + com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOADED, + primaryVol.getKey(), + primaryVol.getKey()); + answer.setTemplateSize(priVolInfo.allocation); + return answer; + } catch (LibvirtException e) { + result = "Failed to download template: " + e.toString(); + s_logger.debug(result); + return new Answer(cmd, false, result); + } catch (URISyntaxException e) { + // TODO Auto-generated catch block + return new Answer(cmd, false, e.toString()); + } finally { + try { + if (primaryVol != null) { + primaryVol.free(); + } + + if (primaryPool != null) { + primaryPool.free(); + } + + if (tmplVol != null) { + tmplVol.free(); + } + + if (secondaryPool != null) { + secondaryPool.destroy(); + secondaryPool.undefine(); + secondaryPool.free(); + } + } catch (LibvirtException l) { + + } + } + + } + + private StoragePool createNfsStoragePool(Connect conn, StoragePoolVO pool) { + String targetPath = _mountPoint + File.separator + pool.getUuid(); + LibvirtStoragePoolDef spd = new LibvirtStoragePoolDef(poolType.NFS, pool.getUuid(), pool.getUuid(), + pool.getHostAddress(), pool.getPath(), targetPath); + File tpFile = new File(targetPath); + if (!tpFile.exists()) { + tpFile.mkdir(); + } + StoragePool sp = null; + try { + s_logger.debug(spd.toString()); + sp = conn.storagePoolDefineXML(spd.toString(), 0); + sp.create(0); + return sp; + } catch (LibvirtException e) { + s_logger.debug(e.toString()); + if (sp != null) { + try { + sp.undefine(); + sp.free(); + } catch (LibvirtException l) { + s_logger.debug("Failed to define nfs storage pool with: " + l.toString()); + } + } + return null; + } + } + + private StoragePool getStoragePool(Connect conn, StoragePoolVO pool) { + StoragePool sp = null; + try { + sp = conn.storagePoolLookupByUUIDString(pool.getUuid()); + } catch (LibvirtException e) { + + } + + if (sp == null) { + if (pool.getPoolType() == StoragePoolType.NetworkFilesystem) { + sp = createNfsStoragePool(conn, pool); + } + if (sp == null) { + s_logger.debug("Failed to create storage Pool"); + return null; + } + } + + try { + StoragePoolInfo spi = sp.getInfo(); + if (spi.state != StoragePoolState.VIR_STORAGE_POOL_RUNNING) { + sp.create(0); + } + } catch (LibvirtException e) { + + } + return sp; + } + + protected Answer execute(ModifyStoragePoolCommand cmd) { + StoragePool storagePool = getStoragePool(_conn, cmd.getPool()); + if (storagePool == null) { + return new Answer(cmd, false, " Failed to create storage pool"); + } + + StoragePoolInfo spi = null; + try { + spi = storagePool.getInfo(); + } catch (LibvirtException e) { + + } + + Map tInfo = new HashMap(); + ModifyStoragePoolAnswer answer = new ModifyStoragePoolAnswer(cmd, + spi.capacity, + spi.allocation, + tInfo); + try { + storagePool.free(); + } catch (LibvirtException e) { + + } + return answer; + } + private Answer execute(NetworkIngressRulesCmd cmd) { + return new NetworkIngressRuleAnswer(cmd); + } + + protected GetVncPortAnswer execute(GetVncPortCommand cmd) { + return new GetVncPortAnswer(cmd, 5900 + getVncPort(cmd.getName())); + } + + protected Answer execute(final CheckConsoleProxyLoadCommand cmd) { + return executeProxyLoadScan(cmd, cmd.getProxyVmId(), cmd.getProxyVmName(), cmd.getProxyManagementIp(), cmd.getProxyCmdPort()); + } + + protected Answer execute(final WatchConsoleProxyLoadCommand cmd) { + return executeProxyLoadScan(cmd, cmd.getProxyVmId(), cmd.getProxyVmName(), cmd.getProxyManagementIp(), cmd.getProxyCmdPort()); + } + + protected MaintainAnswer execute(MaintainCommand cmd) { + return new MaintainAnswer(cmd); + } + + private Answer executeProxyLoadScan(final Command cmd, final long proxyVmId, final String proxyVmName, final String proxyManagementIp, final int cmdPort) { + String result = null; + + final StringBuffer sb = new StringBuffer(); + sb.append("http://").append(proxyManagementIp).append(":" + cmdPort).append("/cmd/getstatus"); + + boolean success = true; + try { + final URL url = new URL(sb.toString()); + final URLConnection conn = url.openConnection(); + + final InputStream is = conn.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + final StringBuilder sb2 = new StringBuilder(); + String line = null; + try { + while ((line = reader.readLine()) != null) + sb2.append(line + "\n"); + result = sb2.toString(); + } catch (final IOException e) { + success = false; + } finally { + try { + is.close(); + } catch (final IOException e) { + s_logger.warn("Exception when closing , console proxy address : " + proxyManagementIp); + success = false; + } + } + } catch(final IOException e) { + s_logger.warn("Unable to open console proxy command port url, console proxy address : " + proxyManagementIp); + success = false; + } + + return new ConsoleProxyLoadAnswer(cmd, proxyVmId, proxyVmName, success, result); + } + + private Answer execute(StartConsoleProxyCommand cmd) { + final ConsoleProxyVO router = cmd.getProxy(); + String result = null; + + State state = State.Stopped; + synchronized(_vms) { + _vms.put(cmd.getVmName(), State.Starting); + } + try { + result = startConsoleProxy(cmd); + if (result != null) { + throw new ExecutionException(result, null); + } + + result = _virtRouterResource.connect(router.getPrivateIpAddress(), cmd.getProxyCmdPort()); + if (result != null) { + throw new ExecutionException(result, null); + } + + state = State.Running; + return new StartConsoleProxyAnswer(cmd, router.getPrivateIpAddress(), router.getPrivateMacAddress()); + } catch (final ExecutionException e) { + return new Answer(cmd, false, e.getMessage()); + } catch (final Throwable th) { + s_logger.warn("Exception while starting router.", th); + return createErrorAnswer(cmd, "Unable to start router", th); + } finally { + synchronized(_vms) { + _vms.put(cmd.getVmName(), state); + } + } + } + + private Answer execute(AttachIsoCommand cmd) { + try { + attachOrDetachISO(cmd.getVmName(), cmd.getIsoPath(), cmd.isAttach()); + } catch (LibvirtException e) { + return new Answer(cmd, false, e.toString()); + } catch (URISyntaxException e) { + return new Answer(cmd, false, e.toString()); + } + + return new Answer(cmd); + } + + private AttachVolumeAnswer execute(AttachVolumeCommand cmd) { + try { + attachOrDetachDisk(cmd.getAttach(), cmd.getVmName(), cmd.getVolumePath()); + } catch (LibvirtException e) { + return new AttachVolumeAnswer(cmd, e.toString()); + } + + return new AttachVolumeAnswer(cmd, cmd.getDeviceId()); + } + + protected static List findVolumes(final List volumes, final Volume.VolumeType vType, boolean singleVolume) { + List filteredVolumes = new ArrayList(); + + if (volumes == null) + return filteredVolumes; + + for (final VolumeVO v: volumes) { + if (v.getVolumeType() == vType) { + filteredVolumes.add(v); + + if(singleVolume) + return filteredVolumes; + } + } + + return filteredVolumes; + } + + private Answer execute(StartRouterCommand cmd) { + + final DomainRouter router = cmd.getRouter(); + + String result = null; + + State state = State.Stopped; + synchronized(_vms) { + _vms.put(cmd.getVmName(), State.Starting); + } + try { + result = startDomainRouter(cmd); + if (result != null) { + throw new ExecutionException(result, null); + } + + result = _virtRouterResource.connect(router.getPrivateIpAddress()); + if (result != null) { + throw new ExecutionException(result, null); + } + + state = State.Running; + return new StartRouterAnswer(cmd); + } catch (final ExecutionException e) { + return new Answer(cmd, false, e.getMessage()); + } catch (final Throwable th) { + return createErrorAnswer(cmd, "Unable to start router", th); + } finally { + synchronized(_vms) { + _vms.put(cmd.getVmName(), state); + } + } + } + + private Answer execute(ReadyCommand cmd) { + return new ReadyAnswer(cmd); + } + + protected State convertToState(DomainInfo.DomainState ps) { + final State state = s_statesTable.get(ps); + return state == null ? State.Unknown : state; + } + + protected State getVmState(final String vmName) { + int retry = 3; + Domain vms = null; + while (retry-- > 0) { + try { + vms = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + State s = convertToState(vms.getInfo().state); + return s; + } catch (final LibvirtException e) { + s_logger.warn("Can't get vm state " + vmName + e.getMessage() + "retry:" + retry); + } finally { + try { + if (vms != null) { + vms.free(); + } + } catch (final LibvirtException e) { + + } + } + } + return State.Stopped; + } + + private Answer execute(CheckVirtualMachineCommand cmd) { + final State state = getVmState(cmd.getVmName()); + Integer vncPort = null; + if (state == State.Running) { + vncPort = getVncPort(cmd.getVmName()); + synchronized(_vms) { + _vms.put(cmd.getVmName(), State.Running); + } + } + + return new CheckVirtualMachineAnswer(cmd, state, vncPort); + } + + private Answer execute(PingTestCommand cmd) { + String result = null; + final String computingHostIp = cmd.getComputingHostIp(); //TODO, split the command into 2 types + + if (computingHostIp != null) { + result = doPingTest(computingHostIp); + } else { + result = doPingTest(cmd.getRouterIp(), cmd.getPrivateIp()); + } + + if (result != null) { + return new Answer(cmd, false, result); + } + return new Answer(cmd); + } + + private String doPingTest( final String computingHostIp ) { + final Script command = new Script(_pingTestPath, 10000, s_logger); + command.add("-h", computingHostIp); + return command.execute(); + } + + private String doPingTest( final String domRIp, final String vmIp ) { + final Script command = new Script(_pingTestPath, 10000, s_logger); + command.add("-i", domRIp); + command.add("-p", vmIp); + return command.execute(); + } + + private Answer execute(MigrateCommand cmd) { + String vmName = cmd.getVmName(); + + State state = null; + String result = null; + synchronized(_vms) { + state = _vms.get(vmName); + _vms.put(vmName, State.Stopping); + } + + Domain dm = null; + Connect dconn = null; + Domain destDomain = null; + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + dconn = new Connect("qemu+tcp://" + cmd.getDestinationIp() + "/system"); + /*Hard code lm flags: VIR_MIGRATE_LIVE(1<<0) and VIR_MIGRATE_PERSIST_DEST(1<<3)*/ + destDomain = dm.migrate(dconn, (1<<0)|(1<<3), vmName, "tcp:" + cmd.getDestinationIp(), 0); + } catch (LibvirtException e) { + s_logger.debug("Can't migrate domain: " + e.getMessage()); + result = e.getMessage(); + } finally { + try { + if (dm != null) + dm.free(); + if (dconn != null) + dconn.close(); + if (destDomain != null) + destDomain.free(); + } catch (final LibvirtException e) { + + } + } + + if (result != null) { + synchronized(_vms) { + _vms.put(vmName, state); + } + } else { + cleanupVM(vmName, getVnetId(VirtualMachineName.getVnet(vmName))); + } + + return new MigrateAnswer(cmd, result == null, result, null); + } + + private Answer execute(PrepareForMigrationCommand cmd) { + final String vmName = cmd.getVmName(); + String result = null; + + if (cmd.getVnet() != null && !isDirectAttachedNetwork(cmd.getVnet())) { + final String vnet = getVnetId(cmd.getVnet()); + if (vnet != null) { + try { + createVnet(vnet, _pifs.first()); /*TODO: Need to add public network for domR*/ + } catch (InternalErrorException e) { + return new PrepareForMigrationAnswer(cmd, false, result); + } + } + } + + synchronized(_vms) { + _vms.put(vmName, State.Migrating); + } + + return new PrepareForMigrationAnswer(cmd, result == null, result); + } + + public void createVnet(String vnetId, String pif) throws InternalErrorException { + final Script command = new Script(_modifyVlanPath, _timeout, s_logger); + command.add("-v", vnetId); + command.add("-p", pif); + command.add("-o", "add"); + + final String result = command.execute(); + if (result != null) { + throw new InternalErrorException("Failed to create vnet " + vnetId + ": " + result); + } + } + + private Answer execute(CheckHealthCommand cmd) { + return new CheckHealthAnswer(cmd, true); + } + + private Answer execute(GetHostStatsCommand cmd) { + + + final Script cpuScript = new Script("/bin/bash", s_logger); + cpuScript.add("-c"); + cpuScript.add("idle=$(top -b -n 1|grep Cpu\\(s\\):|cut -d% -f4|cut -d, -f2);echo $idle"); + + final OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + String result = cpuScript.execute(parser); + if (result != null) { + s_logger.info("Unable to get the host CPU state: " + result); + return new Answer(cmd, false, result); + } + double cpuUtil = (100.0D - Double.parseDouble(parser.getLine())); + + long freeMem = 0; + final Script memScript = new Script("/bin/bash", s_logger); + memScript.add("-c"); + memScript.add("freeMem=$(free|grep cache:|awk '{print $4}');echo $freeMem"); + final OutputInterpreter.OneLineParser Memparser = new OutputInterpreter.OneLineParser(); + result = memScript.execute(Memparser); + if (result != null) { + s_logger.info("Unable to get the host Mem state: " + result); + return new Answer(cmd, false, result); + } + freeMem = Long.parseLong(Memparser.getLine()); + + Script totalMem = new Script("/bin/bash", s_logger); + totalMem.add("-c"); + totalMem.add("free|grep Mem:|awk '{print $2}'"); + final OutputInterpreter.OneLineParser totMemparser = new OutputInterpreter.OneLineParser(); + result = totalMem.execute(totMemparser); + if (result != null) { + s_logger.info("Unable to get the host Mem state: " + result); + return new Answer(cmd, false, result); + } + long totMem = Long.parseLong(totMemparser.getLine()); + + double rx = 0.0; + OutputInterpreter.OneLineParser rxParser = new OutputInterpreter.OneLineParser(); + result = executeBashScript("cat /sys/class/net/" + _publicBridgeName + "/statistics/rx_bytes", rxParser); + if (result == null && rxParser.getLine() != null) { + rx = Double.parseDouble(rxParser.getLine())/1000; + } + + double tx = 0.0; + OutputInterpreter.OneLineParser txParser = new OutputInterpreter.OneLineParser(); + result = executeBashScript("cat /sys/class/net/" + _publicBridgeName + "/statistics/tx_bytes", txParser); + if (result == null && txParser.getLine() != null) { + tx = Double.parseDouble(txParser.getLine())/1000; + } + + int numCpus = 0; + try { + NodeInfo node = _conn.nodeInfo(); + numCpus = node.cpus; + } catch (LibvirtException e) { + + } + + HostStatsEntry hostStats = new HostStatsEntry(cmd.getHostId(), cpuUtil, rx, tx, numCpus, "host", totMem, freeMem, 0, 0); + return new GetHostStatsAnswer(cmd, hostStats); + } + + private Answer execute(RebootCommand cmd) { + Long bytesReceived = null; + Long bytesSent = null; + + synchronized(_vms) { + _vms.put(cmd.getVmName(), State.Starting); + } + + try { + final String result = rebootVM(cmd.getVmName()); + if (result == null) { + /*TODO: need to reset iptables rules*/ + return new RebootAnswer(cmd, null, bytesSent, bytesReceived, getVncPort(cmd.getVmName())); + } else { + return new RebootAnswer(cmd, result); + } + } finally { + synchronized(_vms) { + _vms.put(cmd.getVmName(), State.Running); + } + } + } + + protected Answer execute(RebootRouterCommand cmd) { + RebootAnswer answer = (RebootAnswer) execute((RebootCommand) cmd); + String result = _virtRouterResource.connect(cmd.getPrivateIpAddress()); + if (result == null) { + return answer; + } else { + return new Answer(cmd, false, result); + } + } + + protected GetVmStatsAnswer execute(GetVmStatsCommand cmd) { + List vmNames = cmd.getVmNames(); + try { + HashMap vmStatsNameMap = new HashMap(); + + for (String vmName : vmNames) { + VmStatsEntry statEntry = getVmStat(vmName); + if( statEntry == null ) + continue; + + vmStatsNameMap.put(vmName, statEntry); + } + return new GetVmStatsAnswer(cmd, vmStatsNameMap); + }catch (LibvirtException e) { + s_logger.debug("Can't get vm stats: " + e.toString()); + return new GetVmStatsAnswer(cmd, null); + } + } + + + protected Answer execute(StopCommand cmd) { + StopAnswer answer = null; + final String vmName = cmd.getVmName(); + + final Integer port = getVncPort(vmName); + Long bytesReceived = new Long(0); + Long bytesSent = new Long(0); + + State state = null; + synchronized(_vms) { + state = _vms.get(vmName); + _vms.put(vmName, State.Stopping); + } + try { + /*if (isDirectAttachedNetwork(cmd.getVnet())) + destroy_network_rules_for_vm(vmName);*/ + String result = stopVM(vmName, defineOps.UNDEFINE_VM); + + answer = new StopAnswer(cmd, null, port, bytesSent, bytesReceived); + + if (result != null) { + answer = new StopAnswer(cmd, result, port, bytesSent, bytesReceived); + } + + final String result2 = cleanupVnet(cmd.getVnet()); + if (result2 != null) { + result = result2 + (result != null ? ("\n" + result) : "") ; + answer = new StopAnswer(cmd, result, port, bytesSent, bytesReceived); + } + + + return answer; + } finally { + if (answer == null || !answer.getResult()) { + synchronized(_vms) { + _vms.put(vmName, state); + } + } + } + } + + protected Answer execute(ModifySshKeysCommand cmd) { + File sshKeysDir = new File(_SSHKEYSPATH); + String result = null; + if (!sshKeysDir.exists()) { + sshKeysDir.mkdir(); + // Change permissions for the 600 + Script script = new Script("chmod", _timeout, s_logger); + script.add("600", _SSHKEYSPATH); + script.execute(); + } + + File pubKeyFile = new File(_SSHPUBKEYPATH); + if (!pubKeyFile.exists()) { + try { + pubKeyFile.createNewFile(); + } catch (IOException e) { + result = "Failed to create file: " + e.toString(); + s_logger.debug(result); + } + } + + if (pubKeyFile.exists()) { + String pubKey = cmd.getPubKey(); + try { + FileOutputStream pubkStream = new FileOutputStream(pubKeyFile); + pubkStream.write(pubKey.getBytes()); + pubkStream.close(); + } catch (FileNotFoundException e) { + result = "File" + _SSHPUBKEYPATH + "is not found:" + e.toString(); + s_logger.debug(result); + } catch (IOException e) { + result = "Write file " + _SSHPUBKEYPATH + ":" + e.toString(); + s_logger.debug(result); + } + } + + File prvKeyFile = new File(_SSHPRVKEYPATH); + if (!prvKeyFile.exists()) { + try { + prvKeyFile.createNewFile(); + } catch (IOException e) { + result = "Failed to create file: " + e.toString(); + s_logger.debug(result); + } + } + + if (prvKeyFile.exists()) { + String prvKey = cmd.getPrvKey(); + try { + FileOutputStream prvKStream = new FileOutputStream(prvKeyFile); + prvKStream.write(prvKey.getBytes()); + prvKStream.close(); + } catch (FileNotFoundException e) { + result = "File" + _SSHPRVKEYPATH + "is not found:" + e.toString(); + s_logger.debug(result); + } catch (IOException e) { + result = "Write file " + _SSHPRVKEYPATH + ":" + e.toString(); + s_logger.debug(result); + } + + Script script = new Script("chmod", _timeout, s_logger); + script.add("600", _SSHPRVKEYPATH); + script.execute(); + } + + if (result != null) + return new Answer(cmd, false, result); + else + return new Answer(cmd, true, null); + } + + protected StartAnswer execute(StartCommand cmd) { + final String vmName = cmd.getVmName(); + String result = null; + + State state = State.Stopped; + synchronized(_vms) { + _vms.put(vmName, State.Starting); + } + + try { + result = startVM(cmd); + if (result != null) { + return new StartAnswer(cmd, result); + } + + state = State.Running; + return new StartAnswer(cmd); + } finally { + synchronized(_vms) { + _vms.put(vmName, state); + } + } + } + + private StorageVol getVolume(Connect conn, String volPath) throws LibvirtException, URISyntaxException { + int index = volPath.lastIndexOf("/"); + URI volDir = null; + StoragePool sp = null; + StorageVol vol = null; + try { + volDir = new URI(volPath.substring(0, index)); + String volName = volPath.substring(index + 1); + sp = getNfsSPbyURI(_conn, volDir); + vol = sp.storageVolLookupByName(volName); + return vol; + } catch (LibvirtException e) { + s_logger.debug("Faild to get vol path: " + e.toString()); + throw e; + } finally { + try { + if (sp != null) + sp.free(); + } catch (LibvirtException e) { + + } + } + } + + protected synchronized String startVM(StartCommand cmd) { + List nics = null; + try { + + String uuid = UUID.nameUUIDFromBytes(cmd.getVmName().getBytes()).toString(); + + nics = createUserVMNetworks(cmd); + + List disks = createVMDisk(cmd.getVolumes(), cmd.getGuestOSDescription(), cmd.getISOPath()); + + + + String vmDomainXML = defineVMXML(cmd.getVmName(), uuid, cmd.getRamSize(), cmd.getCpu(), cmd.getArch(), + nics, disks, cmd.getVncPassword(), + cmd.getGuestOSDescription()); + + s_logger.debug(vmDomainXML); + + // Start the domain + startDomain(cmd.getVmName(), vmDomainXML); + + // Attach each data volume to the VM, if there is a deferred attached disk + for (diskDef disk : disks) { + if (disk.isAttachDeferred()) { + attachOrDetachDisk(true, cmd.getVmName(), disk.getDiskPath()); + } + } + + /*if (isDirectAttachedNetwork(cmd.getGuestNetworkId())) + default_network_rules(cmd.getVmName(), cmd.getGuestIpAddress());*/ + + return null; + } catch(LibvirtException e) { + if (nics != null) + cleanupVMNetworks(nics); + s_logger.error("Unable to start VM: ", e); + return "Unable to start VM due to: " + e.getMessage(); + } catch (InternalErrorException e) { + if (nics != null) + cleanupVMNetworks(nics); + s_logger.error("Unable to start VM: ", e); + return "Unable to start VM due to: " + e.getMessage(); + } catch (URISyntaxException e) { + if (nics != null) + cleanupVMNetworks(nics); + s_logger.error("Unable to start VM: ", e); + return "Unable to start VM due to: " + e.getMessage(); + } + } + + protected synchronized String attachOrDetachISO(String vmName, String isoPath, boolean isAttach) throws LibvirtException, URISyntaxException { + String isoXml = null; + if (isoPath != null && isAttach) { + StorageVol isoVol = getVolume(_conn, isoPath); + + isoPath = isoVol.getPath(); + + diskDef iso = new diskDef(); + iso.defFileBasedDisk(isoPath, "hdc", diskDef.diskBus.IDE, diskDef.diskFmtType.RAW); + iso.setDeviceType(diskDef.deviceType.CDROM); + isoXml = iso.toString(); + } else { + diskDef iso = new diskDef(); + iso.defFileBasedDisk(null, "hdc", diskDef.diskBus.IDE, diskDef.diskFmtType.RAW); + iso.setDeviceType(diskDef.deviceType.CDROM); + isoXml = iso.toString(); + } + + return attachOrDetachDevice(true, vmName, isoXml); + } + + protected synchronized String attachOrDetachDisk(boolean attach, String vmName, String sourceFile) throws LibvirtException { + if (isCentosHost()) { + return "disk hotplug is not supported by hypervisor"; + } + String diskDev = null; + SortedMap diskMaps = null; + Domain dm = null; + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + String xml = dm.getXMLDesc(0); + parser.parseDomainXML(xml); + diskMaps = parser.getDiskMaps(); + } catch (LibvirtException e) { + throw e; + } finally { + if (dm != null) { + dm.free(); + } + } + + if (attach) { + diskDev = diskMaps.lastKey(); + /*Find the latest disk dev, and add 1 on it: e.g. if we already attach sdc to a vm, the next disk dev is sdd*/ + diskDev = diskDev.substring(0, diskDev.length() - 1) + (char)(diskDev.charAt(diskDev.length() -1) + 1); + } else { + Set> entrySet = diskMaps.entrySet(); + Iterator> itr = entrySet.iterator(); + while (itr.hasNext()) { + Map.Entry entry = itr.next(); + if (entry.getValue().equalsIgnoreCase(sourceFile)) { + diskDev = entry.getKey(); + break; + } + } + } + + if (diskDev == null) { + s_logger.warn("Can't get disk dev"); + return "Can't get disk dev"; + } + diskDef disk = new diskDef(); + String guestOSType = getGuestType(vmName); + if (isGuestPVEnabled(guestOSType)) { + disk.defFileBasedDisk(sourceFile, diskDev, diskDef.diskBus.VIRTIO, diskDef.diskFmtType.QCOW2); + } else { + disk.defFileBasedDisk(sourceFile, diskDev, diskDef.diskBus.SCSI, diskDef.diskFmtType.QCOW2); + } + String xml = disk.toString(); + return attachOrDetachDevice(attach, vmName, xml); + } + + private synchronized String attachOrDetachDevice(boolean attach, String vmName, String xml) throws LibvirtException{ + Domain dm = null; + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes((vmName.getBytes()))); + + if (attach) { + s_logger.debug("Attaching device: " + xml); + dm.attachDevice(xml); + } else { + s_logger.debug("Detaching device: " + xml); + dm.detachDevice(xml); + } + } catch (LibvirtException e) { + if (attach) + s_logger.warn("Failed to attach device to " + vmName + ": " + e.getMessage()); + else + s_logger.warn("Failed to detach device from " + vmName + ": " + e.getMessage()); + throw e; + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException l) { + + } + } + } + + return null; + } + + @Override + public PingCommand getCurrentStatus(long id) { + final HashMap newStates = sync(); + return new PingRoutingCommand(com.cloud.host.Host.Type.Routing, id, newStates); + } + + @Override + public Type getType() { + return Type.Routing; + } + + private Map getVersionStrings() { + final Script command = new Script(_versionstringpath, _timeout, s_logger); + KeyValueInterpreter kvi = new KeyValueInterpreter(); + String result = command.execute(kvi); + if (result == null) { + return kvi.getKeyValues(); + }else { + return new HashMap(1); + } + } + + @Override + public StartupCommand [] initialize() { + Map changes = null; + + synchronized(_vms) { + _vms.clear(); + changes = sync(); + } + + final List info = getHostInfo(); + + final StartupRoutingCommand cmd = new StartupRoutingCommand((Integer)info.get(0), (Long)info.get(1), (Long)info.get(2), (Long)info.get(4), (String)info.get(3), Hypervisor.Type.KVM, RouterPrivateIpStrategy.HostLocal, changes); + fillNetworkInformation(cmd); + cmd.getHostDetails().putAll(getVersionStrings()); + cmd.setPool(_pool); + + return new StartupCommand[]{cmd}; + } + + protected HashMap sync() { + HashMap newStates; + HashMap oldStates = null; + + final HashMap changes = new HashMap(); + + synchronized(_vms) { + newStates = getAllVms(); + if (newStates == null) { + s_logger.debug("Unable to get the vm states so no state sync at this point."); + return changes; + } + + oldStates = new HashMap(_vms.size()); + oldStates.putAll(_vms); + + for (final Map.Entry entry : newStates.entrySet()) { + final String vm = entry.getKey(); + + State newState = entry.getValue(); + final State oldState = oldStates.remove(vm); + + if (newState == State.Stopped && oldState != State.Stopping && oldState != null && oldState != State.Stopped) { + newState = getRealPowerState(vm); + } + + if (s_logger.isTraceEnabled()) { + s_logger.trace("VM " + vm + ": libvirt has state " + newState + " and we have state " + (oldState != null ? oldState.toString() : "null")); + } + + if (vm.startsWith("migrating")) { + s_logger.debug("Migration detected. Skipping"); + continue; + } + if (oldState == null) { + _vms.put(vm, newState); + s_logger.debug("Detecting a new state but couldn't find a old state so adding it to the changes: " + vm); + changes.put(vm, newState); + } else if (oldState == State.Starting) { + if (newState == State.Running) { + _vms.put(vm, newState); + } else if (newState == State.Stopped) { + s_logger.debug("Ignoring vm " + vm + " because of a lag in starting the vm." ); + } + } else if (oldState == State.Migrating) { + if (newState == State.Running) { + s_logger.debug("Detected that an migrating VM is now running: " + vm); + _vms.put(vm, newState); + } + } else if (oldState == State.Stopping) { + if (newState == State.Stopped) { + _vms.put(vm, newState); + } else if (newState == State.Running) { + s_logger.debug("Ignoring vm " + vm + " because of a lag in stopping the vm. "); + } + } else if (oldState != newState) { + _vms.put(vm, newState); + if (newState == State.Stopped) { + if (_vmsKilled.remove(vm)) { + s_logger.debug("VM " + vm + " has been killed for storage. "); + newState = State.Error; + } + } + changes.put(vm, newState); + } + } + + for (final Map.Entry entry : oldStates.entrySet()) { + final String vm = entry.getKey(); + final State oldState = entry.getValue(); + + if (s_logger.isTraceEnabled()) { + s_logger.trace("VM " + vm + " is now missing from libvirt so reporting stopped"); + } + + if (oldState == State.Stopping) { + s_logger.debug("Ignoring VM " + vm + " in transition state stopping."); + _vms.remove(vm); + } else if (oldState == State.Starting) { + s_logger.debug("Ignoring VM " + vm + " in transition state starting."); + } else if (oldState == State.Stopped) { + _vms.remove(vm); + } else if (oldState == State.Migrating) { + s_logger.debug("Ignoring VM " + vm + " in migrating state."); + } else { + State state = State.Stopped; + if (_vmsKilled.remove(entry.getKey())) { + s_logger.debug("VM " + vm + " has been killed by storage monitor"); + state = State.Error; + } + changes.put(entry.getKey(), state); + } + } + } + + return changes; + } + + protected State getRealPowerState(String vm) { + int i = 0; + s_logger.trace("Checking on the HALTED State"); + Domain dm = null; + for (; i < 5; i++) { + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vm.getBytes())); + DomainInfo.DomainState vps = dm.getInfo().state; + if (vps != null && vps != DomainInfo.DomainState.VIR_DOMAIN_SHUTOFF && + vps != DomainInfo.DomainState.VIR_DOMAIN_NOSTATE) { + return convertToState(vps); + } + } catch (final LibvirtException e) { + s_logger.trace(e.getMessage()); + } finally { + try { + if (dm != null) + dm.free(); + } catch (final LibvirtException e) { + + } + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + } + return State.Stopped; + } + + protected List getAllVmNames() { + ArrayList la = new ArrayList(); + try { + final String names[] = _conn.listDefinedDomains(); + for (int i = 0; i < names.length; i++) { + la.add(names[i]); + } + } catch (final LibvirtException e) { + s_logger.warn("Failed to list Defined domains", e); + } + + int[] ids = null; + try { + ids = _conn.listDomains(); + } catch (final LibvirtException e) { + s_logger.warn("Failed to list domains", e); + return la; + } + + Domain dm = null; + for (int i = 0 ; i < ids.length; i++) { + try { + dm = _conn.domainLookupByID(ids[i]); + la.add(dm.getName()); + } catch (final LibvirtException e) { + s_logger.warn("Unable to get vms", e); + } finally { + try { + if (dm != null) + dm.free(); + } catch (final LibvirtException e) { + + } + } + } + + return la; + } + + + private HashMap getAllVms() { + final HashMap vmStates = new HashMap(); + + String[] vms = null; + int[] ids = null; + try { + ids = _conn.listDomains(); + } catch (final LibvirtException e) { + s_logger.warn("Unable to listDomains", e); + return null; + } + try { + vms = _conn.listDefinedDomains(); + } catch (final LibvirtException e){ + s_logger.warn("Unable to listDomains", e); + return null; + } + + Domain dm = null; + for (int i =0; i < ids.length; i++) { + try { + s_logger.debug("domid" + ids[i]); + dm = _conn.domainLookupByID(ids[i]); + + DomainInfo.DomainState ps = dm.getInfo().state; + + final State state = convertToState(ps); + + s_logger.trace("VM " + dm.getName() + ": powerstate = " + ps + "; vm state=" + state.toString()); + String vmName = dm.getName(); + vmStates.put(vmName, state); + } catch (final LibvirtException e) { + s_logger.warn("Unable to get vms", e); + } finally { + try { + if (dm != null) + dm.free(); + } catch (LibvirtException e) { + + } + } + } + + for (int i =0 ; i < vms.length; i++) { + try { + + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vms[i].getBytes())); + + DomainInfo.DomainState ps = dm.getInfo().state; + final State state = convertToState(ps); + String vmName = dm.getName(); + s_logger.trace("VM " + vmName + ": powerstate = " + ps + "; vm state=" + state.toString()); + + vmStates.put(vmName, state); + } catch (final LibvirtException e) { + s_logger.warn("Unable to get vms", e); + } finally { + try { + if (dm != null) + dm.free(); + } catch (LibvirtException e) { + + } + } + } + + return vmStates; + } + + protected List getHostInfo() { + final ArrayList info = new ArrayList(); + long speed = 0; + long cpus = 0; + long ram = 0; + String osType = null; + try { + final NodeInfo hosts = _conn.nodeInfo(); + + cpus = hosts.cpus; + speed = hosts.mhz; + ram = hosts.memory * 1024L; + LibvirtCapXMLParser parser = new LibvirtCapXMLParser(); + parser.parseCapabilitiesXML(_conn.getCapabilities()); + ArrayList oss = parser.getGuestOsType(); + for(String s : oss) + /*Even host supports guest os type more than hvm, we only report hvm to management server*/ + if (s.equalsIgnoreCase("hvm")) + osType = "hvm"; + } catch (LibvirtException e) { + + } + + info.add((int)cpus); + info.add(speed); + info.add(ram); + info.add(osType); + long dom0ram = Math.min(ram/10, 768*1024*1024L);//save a maximum of 10% of system ram or 768M + dom0ram = Math.max(dom0ram, _dom0MinMem); + info.add(dom0ram); + s_logger.info("cpus=" + cpus + ", speed=" + speed + ", ram=" + ram + ", dom0ram=" + dom0ram); + + return info; + } + + protected void cleanupVM(final String vmName, final String vnet) { + s_logger.debug("Trying to cleanup the vnet: " + vnet); + if (vnet != null) + cleanupVnet(vnet); + + _vmStats.remove(vmName); + } + + protected String rebootVM(String vmName) { + String msg = stopVM(vmName, defineOps.DEFINE_VM); + + if (msg == null) { + Domain dm = null; + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + dm.create(); + + return null; + } catch (LibvirtException e) { + s_logger.warn("Failed to create vm", e); + msg = e.getMessage(); + } finally { + try { + if (dm != null) + dm.free(); + } catch (LibvirtException e) { + + } + } + } + + return msg; + } + protected String stopVM(String vmName, defineOps df) { + DomainInfo.DomainState state = null; + Domain dm = null; + + s_logger.debug("Try to stop the vm at first"); + String ret = stopVM(vmName, false); + if (ret == Script.ERR_TIMEOUT) { + ret = stopVM(vmName, true); + } else if (ret != null) { + /*There is a race condition between libvirt and qemu: + * libvirt listens on qemu's monitor fd. If qemu is shutdown, while libvirt is reading on + * the fd, then libvirt will report an error. */ + /*Retry 3 times, to make sure we can get the vm's status*/ + for (int i = 0; i < 3; i++) { + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + state = dm.getInfo().state; + break; + } catch (LibvirtException e) { + s_logger.debug("Failed to get vm status:" + e.getMessage()); + } finally { + try { + if (dm != null) + dm.free(); + } catch (LibvirtException l) { + + } + } + } + + if (state == null) { + s_logger.debug("Can't get vm's status, assume it's dead already"); + return null; + } + + if (state != DomainInfo.DomainState.VIR_DOMAIN_SHUTOFF) { + s_logger.debug("Try to destroy the vm"); + ret = stopVM(vmName, true); + if (ret != null) { + return ret; + } + } + } + + if (df == defineOps.UNDEFINE_VM) { + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + dm.undefine(); + } catch (LibvirtException e) { + + } finally { + try { + if (dm != null) + dm.free(); + } catch (LibvirtException l) { + + } + } + } + return null; + } + protected String stopVM(String vmName, boolean force) { + Domain dm = null; + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + if (force) { + if (dm.getInfo().state != DomainInfo.DomainState.VIR_DOMAIN_SHUTOFF) { + dm.destroy(); + } + } else { + if (dm.getInfo().state == DomainInfo.DomainState.VIR_DOMAIN_SHUTOFF) { + return null; + } + dm.shutdown(); + int retry = _stopTimeout/2000; + /*Wait for the domain gets into shutoff state*/ + while ((dm.getInfo().state != DomainInfo.DomainState.VIR_DOMAIN_SHUTOFF) && (retry >= 0)) { + Thread.sleep(2000); + retry--; + } + if (retry < 0) { + s_logger.warn("Timed out waiting for domain " + vmName + " to shutdown gracefully"); + return Script.ERR_TIMEOUT; + } + } + } catch (LibvirtException e) { + s_logger.debug("Failed to stop VM :" + vmName + " :", e); + return e.getMessage(); + } catch (InterruptedException ie) { + s_logger.debug("Interrupted sleep"); + } finally { + try { + if (dm != null) + dm.free(); + } catch (LibvirtException e) { + } + } + + return null; + } + + public synchronized String cleanupVnet(final String vnetId) { + // VNC proxy VMs do not have vnet + if(vnetId == null || vnetId.isEmpty() || isDirectAttachedNetwork(vnetId)) + return null; + + final List names = getAllVmNames(); + + if (!names.isEmpty()) { + for (final String name : names) { + if (VirtualMachineName.getVnet(name).equals(vnetId)) { + return null; // Can't remove the vnet yet. + } + } + } + + final Script command = new Script(_modifyVlanPath, _timeout, s_logger); + command.add("-o", "delete"); + command.add("-v", vnetId); + return command.execute(); + } + + protected Integer getVncPort( String vmName) { + if (VirtualMachineName.isValidRouterName(vmName) || VirtualMachineName.isValidConsoleProxyName(vmName)) { + return null; // no vnc ports for domr + } + LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + Domain dm = null; + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + String xmlDesc = dm.getXMLDesc(0); + parser.parseDomainXML(xmlDesc); + return parser.getVncPort(); + } catch (LibvirtException e) { + + } finally { + try { + if (dm != null) + dm.free(); + } catch (LibvirtException l) { + + } + } + return null; + } + + protected int[] gatherVncPorts(final Collection names) { + final ArrayList ports = new ArrayList(names.size()); + for (final String name : names) { + final Integer port = getVncPort(name); + + if (port != null) { + ports.add(port); + } + } + + final int[] results = new int[ports.size()]; + int i = 0; + for (final Integer port : ports) { + results[i++] = port; + } + + return results; + } + + private boolean IsHVMEnabled() { + LibvirtCapXMLParser parser = new LibvirtCapXMLParser(); + try { + parser.parseCapabilitiesXML(_conn.getCapabilities()); + ArrayList osTypes = parser.getGuestOsType(); + for (String o : osTypes) { + if (o.equalsIgnoreCase("hvm")) + return true; + } + } catch (LibvirtException e) { + + } + return false; + } + + private String getHypervisorPath() { + File f =new File("/usr/bin/cloud-qemu-kvm"); + if (f.exists()) { + return "/usr/bin/cloud-qemu-kvm"; + } else { + if (_conn == null) + return null; + + LibvirtCapXMLParser parser = new LibvirtCapXMLParser(); + try { + parser.parseCapabilitiesXML(_conn.getCapabilities()); + } catch (LibvirtException e) { + + } + return parser.getEmulator(); + } + } + + private String getGuestType(String vmName) { + LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + Domain dm = null; + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + String xmlDesc = dm.getXMLDesc(0); + parser.parseDomainXML(xmlDesc); + return parser.getDescription(); + } catch (LibvirtException e) { + return null; + } finally { + try { + if (dm != null) + dm.free(); + } catch (LibvirtException l) { + + } + } + } + + private boolean isGuestPVEnabled(String guestOS) { + if (guestOS == null) + return false; + if (guestOS.startsWith("Ubuntu 10.04") || + guestOS.startsWith("Ubuntu 9") || + guestOS.startsWith("Ubuntu 8.10") || + guestOS.startsWith("Fedora 13") || + guestOS.startsWith("Fedora 12") || + guestOS.startsWith("Fedora 11") || + guestOS.startsWith("Fedora 10") || + guestOS.startsWith("Fedora 9") || + guestOS.startsWith("CentOS 5.3") || + guestOS.startsWith("CentOS 5.4") || + guestOS.startsWith("CentOS 5.5") || + guestOS.startsWith("Red Hat Enterprise Linux 5.3") || + guestOS.startsWith("Red Hat Enterprise Linux 5.4") || + guestOS.startsWith("Red Hat Enterprise Linux 5.5") || + guestOS.startsWith("Red Hat Enterprise Linux 6") || + guestOS.startsWith("Debain Lenney") || + guestOS.startsWith("Debain Squeeze") + ) + return true; + else + return false; + } + + private boolean isCentosHost() { + if (_hvVersion <=9 ) { + return true; + } else + return false; + } + + private StorageVol createTmplDataDisk(String rootkPath, long size) throws LibvirtException, InternalErrorException { + /*create a templ data disk, to contain patches*/ + StorageVol rootVol = getVolume(rootkPath); + StoragePool rootPool = rootVol.storagePoolLookupByVolume(); + LibvirtStorageVolumeDef volDef = new LibvirtStorageVolumeDef(UUID.randomUUID().toString(), size, volFormat.RAW, null, null); + StorageVol dataVol = rootPool.storageVolCreateXML(volDef.toString(), 0); + + /*Format/create fs on this disk*/ + final Script command = new Script(_createvmPath, _timeout, s_logger); + command.add("-f", dataVol.getKey()); + String result = command.execute(); + if (result != null) { + s_logger.debug("Failed to create data disk: " + result); + throw new InternalErrorException("Failed to create data disk: " + result); + } + return dataVol; + } + + private interfaceDef.nicModel getGuestNicModel(String guestOSType) { + if (isGuestPVEnabled(guestOSType) && !isCentosHost()) { + return interfaceDef.nicModel.VIRTIO; + } else { + return interfaceDef.nicModel.E1000; + } + } + + private diskDef.diskBus getGuestDiskModel(String guestOSType) { + if (isGuestPVEnabled(guestOSType) && !isCentosHost()) { + return diskDef.diskBus.VIRTIO; + } else { + return diskDef.diskBus.IDE; + } + } + + private String setVnetBrName(String vnetId) { + return "cloudVirBr" + vnetId; + } + private String getVnetIdFromBrName(String vnetBrName) { + return vnetBrName.replaceAll("cloudVirBr", ""); + } + private List createUserVMNetworks(StartCommand cmd) throws InternalErrorException { + List nics = new ArrayList(); + interfaceDef.nicModel nicModel = getGuestNicModel(cmd.getGuestOSDescription()); + String guestMac = cmd.getGuestMacAddress(); + String brName; + interfaceDef pubNic = new interfaceDef(); + if (cmd.getGuestIpAddress() == null) { + /*guest network is direct attached without external DHCP server*/ + brName = _privBridgeName; + pubNic.setHostNetType(hostNicType.DIRECT_ATTACHED_WITHOUT_DHCP); + } else if ("untagged".equalsIgnoreCase(cmd.getGuestNetworkId())){ + /*guest network is direct attached with domr DHCP server*/ + brName = _privBridgeName; + pubNic.setHostNetType(hostNicType.DIRECT_ATTACHED_WITH_DHCP); + } else { + /*guest network is vnet*/ + String vnetId = getVnetId(cmd.getGuestNetworkId()); + brName = setVnetBrName(vnetId); + createVnet(vnetId, _pifs.first()); + pubNic.setHostNetType(hostNicType.VLAN); + } + pubNic.defBridgeNet(brName, null, guestMac, nicModel); + nics.add(pubNic); + return nics; + } + + private List createRouterVMNetworks(StartRouterCommand cmd) throws InternalErrorException { + List nics = new ArrayList(); + DomainRouter router = cmd.getRouter(); + String guestMac = router.getGuestMacAddress(); + String privateMac = router.getPrivateMacAddress(); + String pubMac = router.getPublicMacAddress(); + String brName; + interfaceDef pubNic = new interfaceDef(); + interfaceDef privNic = new interfaceDef(); + interfaceDef vnetNic = new interfaceDef(); + + /*nic 0, guest network*/ + if ("untagged".equalsIgnoreCase(router.getVnet())){ + vnetNic.defBridgeNet(_privBridgeName, null, guestMac, interfaceDef.nicModel.VIRTIO); + + } else { + String vnetId = getVnetId(router.getVnet()); + brName = setVnetBrName(vnetId); + String vnetDev = "vtap" + vnetId; + createVnet(vnetId, _pifs.first()); + vnetNic.defBridgeNet(brName, vnetDev, guestMac, interfaceDef.nicModel.VIRTIO); + } + nics.add(vnetNic); + + /*nic 1: link local*/ + privNic.defPrivateNet(_privNwName, null, privateMac, interfaceDef.nicModel.VIRTIO); + nics.add(privNic); + + /*nic 2: public */ + if ("untagged".equalsIgnoreCase(router.getVlanId())) { + pubNic.defBridgeNet(_publicBridgeName, null, pubMac, interfaceDef.nicModel.VIRTIO); + } else { + String vnetId = getVnetId(router.getVlanId()); + brName = setVnetBrName(vnetId); + String vnetDev = "vtap" + vnetId; + createVnet(vnetId, _pifs.second()); + pubNic.defBridgeNet(brName, vnetDev, pubMac, interfaceDef.nicModel.VIRTIO); + } + nics.add(pubNic); + return nics; + } + + private List createConsoleVMNetworks(StartConsoleProxyCommand cmd) { + List nics = new ArrayList(); + ConsoleProxyVO console = cmd.getProxy(); + String privateMac = console.getPrivateMacAddress(); + String pubMac = console.getPublicMacAddress(); + interfaceDef pubNic = new interfaceDef(); + interfaceDef privNic = new interfaceDef(); + interfaceDef vnetNic = new interfaceDef(); + + /*guest network is vnet: 0 is not used, 1 is link local, 2 is pub nic*/ + vnetNic.defPrivateNet("default", null, null, interfaceDef.nicModel.VIRTIO); + nics.add(vnetNic); + + privNic.defPrivateNet(_privNwName, null, privateMac, interfaceDef.nicModel.VIRTIO); + nics.add(privNic); + + pubNic.defBridgeNet(_publicBridgeName, null, pubMac, interfaceDef.nicModel.VIRTIO); + nics.add(pubNic); + + return nics; + } + + private void cleanupVMNetworks(List nics) { + for (interfaceDef nic : nics) { + if (nic.getHostNetType() == hostNicType.VNET) { + cleanupVnet(getVnetIdFromBrName(nic.getBrName())); + } + } + } + + private List createSystemVMDisk(List vols) throws InternalErrorException, LibvirtException{ + List disks = new ArrayList(); + // Get the root volume + List rootVolumes = findVolumes(vols, VolumeType.ROOT, true); + + if (rootVolumes.size() != 1) { + throw new InternalErrorException("Could not find systemVM root disk."); + } + + VolumeVO rootVolume = rootVolumes.get(0); + String rootkPath = rootVolume.getPath(); + + StorageVol tmplVol = createTmplDataDisk(rootkPath, 10L * 1024 * 1024); + String datadiskPath = tmplVol.getKey(); + + diskDef hda = new diskDef(); + hda.defFileBasedDisk(rootkPath, "vda", diskDef.diskBus.IDE, diskDef.diskFmtType.QCOW2); + disks.add(hda); + + diskDef hdb = new diskDef(); + hdb.defFileBasedDisk(datadiskPath, "hdb", diskDef.diskBus.IDE, diskDef.diskFmtType.RAW); + disks.add(hdb); + + return disks; + } + + private List createVMDisk(List vols, String guestOSType, String isoURI) throws InternalErrorException, LibvirtException, URISyntaxException{ + List disks = new ArrayList(); + // Get the root volume + List rootVolumes = findVolumes(vols, VolumeType.ROOT, true); + + if (rootVolumes.size() != 1) { + throw new InternalErrorException("Could not find UserVM root disk."); + } + + VolumeVO rootVolume = rootVolumes.get(0); + + String isoPath = null; + if (isoURI != null) { + StorageVol isoVol = getVolume(_conn, isoURI); + if (isoVol != null) { + isoPath = isoVol.getPath(); + } else + throw new InternalErrorException("Can't find iso volume"); + } + + List dataVolumes = findVolumes(vols, VolumeType.DATADISK, false); + VolumeVO dataVolume = null; + if (dataVolumes.size() > 0) + dataVolume = dataVolumes.get(0); + + diskDef.diskBus diskBusType = getGuestDiskModel(guestOSType); + + + diskDef hda = new diskDef(); + hda.defFileBasedDisk(rootVolume.getPath(), "vda", diskBusType, diskDef.diskFmtType.QCOW2); + disks.add(hda); + + /*Centos doesn't support scsi hotplug. For other host OSes, we attach the disk after the vm is running, so that we can hotplug it.*/ + if (dataVolume != null) { + diskDef hdb = new diskDef(); + hdb.defFileBasedDisk(dataVolume.getPath(), "vdb", diskBusType, diskDef.diskFmtType.QCOW2); + if (!isCentosHost()) { + hdb.setAttachDeferred(true); + } + disks.add(hdb); + } + + /*Add a placeholder for iso, even if there is no iso attached*/ + diskDef hdc = new diskDef(); + hdc.defFileBasedDisk(isoPath, "hdc", diskDef.diskBus.IDE, diskDef.diskFmtType.RAW); + hdc.setDeviceType(diskDef.deviceType.CDROM); + disks.add(hdc); + + return disks; + } + + private boolean can_bridge_firewall() { + String command = "iptables -N BRIDGE-FIREWALL; " + + "iptables -I BRIDGE-FIREWALL -m state --state RELATED,ESTABLISHED -j ACCEPT"; + + String result = executeBashScript(command); + if (result != null) { + s_logger.debug("Chain BRIDGE-FIREWALL already exists"); + } + + boolean enabled = true; + + result = executeBashScript("iptables -n -L FORWARD|grep BRIDGE-FIREWALL");; + if (result != null) { + result = executeBashScript("iptables -I FORWARD -m physdev --physdev-is-bridged -j BRIDGE-FIREWALL");; + if (result != null) { + enabled = false; + } + } + + File logPath = new File("/var/run/cloud"); + if (!logPath.exists()) { + logPath.mkdir(); + } + + cleanup_rules_for_dead_vms(); + cleanup_rules(); + return enabled; + } + + private void cleanup_rules_for_dead_vms() { + + } + + private void cleanup_rules() { + + } + + /* private void ipset(String ipsetname, String proto, String start, String end, List ips) { + Script command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + command.add("ipset -N " + ipsetname + " iptreemap"); + String result = command.execute(); + if (result != null) { + s_logger.debug("ipset chain already exists: " + ipsetname); + } + boolean success = true; + String ipsettmp = ipsetname + + + } + */ + + private Domain getDomain(String vmName) throws LibvirtException { + return _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + } + + private List getInterfaces(String vmName) { + LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + Domain dm = null; + try { + dm = _conn.domainLookupByUUID(UUID.nameUUIDFromBytes(vmName.getBytes())); + parser.parseDomainXML(dm.getXMLDesc(0)); + } catch (LibvirtException e) { + s_logger.debug("Failed to get dom xml: " + e.toString()); + return new ArrayList(); + } finally { + try { + if (dm != null) { + dm.free(); + } + } catch (LibvirtException e) { + + } + } + + return parser.getInterfaces(); + + } + + private String executeBashScript(String script) { + Script command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + command.add(script); + return command.execute(); + } + + private String executeBashScript(String script, OutputInterpreter parser) { + Script command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + command.add(script); + return command.execute(parser); + } + + private boolean default_network_rules_for_systemvm(String vmName) { + String command; + + List nics = getInterfaces(vmName); + for (String vif : nics) { + command = "iptables -D BRIDGE-FIREWALL -m physdev --physdev-is-bridged --physdev-out " + vif + " -j " + vmName + ";" + + "iptables -D BRIDGE-FIREWALL -m physdev --physdev-is-bridged --physdev-in " + vif + " -j " + vmName; + String result = executeBashScript(command); + if (result != null) { + s_logger.debug("Ingnoring failure to delete old rules"); + } + command = "iptables -N " + vmName; + result = executeBashScript(command); + if (result != null) { + command = "iptables -F " + vmName; + result = executeBashScript(command); + } + command = "iptables -A BRIDGE-FIREWALL -m physdev --physdev-is-bridged --physdev-out " + vif + " -j " + vmName + ";" + + "iptables -A BRIDGE-FIREWALL -m physdev --physdev-is-bridged --physdev-in " + vif + " -j " + vmName; + result = executeBashScript(command); + if (result != null) { + s_logger.debug("Failed to program default rules"); + return false; + } + } + command = "iptables -A " + vmName + " -j ACCEPT"; + executeBashScript(command); + return true; + } + + private boolean default_network_rules(String vmName, String vmIP) { + String command; + + List nics = getInterfaces(vmName); + for (String vif : nics) { + command = "iptables -D BRIDGE-FIREWALL -m physdev --physdev-is-bridged --physdev-out " + vif + " -j " + vmName + ";" + + "iptables -D BRIDGE-FIREWALL -m physdev --physdev-is-bridged --physdev-in " + vif + " -j " + vmName + ";" + + "iptables -F " + vmName + ";" + + "iptables -X " + vmName + ";"; + String result = executeBashScript(command); + if (result != null) { + s_logger.debug("Ignoring failure to delete old rules"); + } + + result = executeBashScript("iptables -N " + vmName); + if (result != null) { + executeBashScript("iptables -F " + vmName); + } + + command = "iptables -D BRIDGE-FIREWALL -m physdev --physdev-is-bridged --physdev-out " + vif + " -j " + vmName + ";" + + "iptables -D BRIDGE-FIREWALL -m physdev --physdev-is-bridged --physdev-in " + vif + " -j " + vmName + ";" + + "iptables -A " + vmName + " -m state --state RELATED,ESTABLISHED -j ACCEPT" + ";" + + "iptables -A " + vmName + " -p udp --dport 67:68 --sport 67:68 -j ACCEPT" + ";" + + "iptables -A " + vmName + " -m physdev --physdev-is-bridged --physdev-in " + vif + " --source " + vmIP + " -j RETURN" + ";" + + "iptables -A " + vmName + " -j DROP"; + result = executeBashScript(command); + if (result != null) { + s_logger.debug("Failed to program default rules for vm:" + vmName); + return false; + } + } + return true; + } + + private boolean destroy_network_rules_for_vm(String vmName) { + String command = "iptables-save |grep BRIDGE-FIREWALL |grep " + vmName + " | sed 's/-A/-D/'"; + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = executeBashScript(command, parser); + + if (result == null && parser.getLines() != null) { + String[] lines = parser.getLines().split("\\n"); + for (String cmd : lines) { + command = "iptables " + cmd; + executeBashScript(command); + } + } + + executeBashScript("iptables -F " + vmName); + executeBashScript("iptables -X " + vmName); + return true; + } + + private void deletExitingLinkLocalRoutTable(String linkLocalBr) { + Script command = new Script("/bin/bash", _timeout); + command.add("-c"); + command.add("ip route | grep " + NetUtils.getLinkLocalCIDR()); + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = command.execute(parser); + boolean foundLinkLocalBr = false; + if (result == null && parser.getLines() != null) { + String[] lines = parser.getLines().split("\\n"); + for (String line : lines) { + String[] tokens = line.split(" "); + if (!tokens[2].equalsIgnoreCase(linkLocalBr)) + Script.runSimpleBashScript("ip route del " + NetUtils.getLinkLocalCIDR()); + else + foundLinkLocalBr = true; + } + } + if (!foundLinkLocalBr) { + Script.runSimpleBashScript("ip route add " + NetUtils.getLinkLocalCIDR() + " dev " + linkLocalBr + " src " + NetUtils.getLinkLocalGateway()); + } + } + + private class vmStats { + long _usedTime; + long _tx; + long _rx; + Calendar _timestamp; + } + + private VmStatsEntry getVmStat(String vmName) throws LibvirtException{ + Domain dm = null; + try { + dm = getDomain(vmName); + DomainInfo info = dm.getInfo(); + + VmStatsEntry stats = new VmStatsEntry(); + stats.setNumCPUs(info.nrVirtCpu); + stats.setEntityType("vm"); + + /*get cpu utilization*/ + vmStats oldStats = null; + + Calendar now = Calendar.getInstance(); + + oldStats = _vmStats.get(vmName); + + + long elapsedTime = 0; + if (oldStats != null) { + elapsedTime = now.getTimeInMillis() - oldStats._timestamp.getTimeInMillis(); + double utilization = (info.cpuTime - oldStats._usedTime)/((double)elapsedTime*1000000); + + NodeInfo node = _conn.nodeInfo(); + utilization = utilization/node.cpus; + stats.setCPUUtilization(utilization*100); + } + + /*get network stats*/ + + List vifs = getInterfaces(vmName); + long rx = 0; + long tx = 0; + for (String vif : vifs) { + DomainInterfaceStats ifStats = dm.interfaceStats(vif); + rx += ifStats.rx_bytes; + tx += ifStats.tx_bytes; + } + + if (oldStats != null) { + stats.setNetworkReadKBs((rx - oldStats._rx)/1000); + stats.setNetworkWriteKBs((tx - oldStats._tx)/1000); + } + + vmStats newStat = new vmStats(); + newStat._usedTime = info.cpuTime; + newStat._rx = rx; + newStat._tx = tx; + newStat._timestamp = now; + _vmStats.put(vmName, newStat); + return stats; + } finally { + if (dm != null) { + dm.free(); + } + } + } + + private StorageVol copyVolume(StoragePool destPool, LibvirtStorageVolumeDef destVol, StorageVol srcVol) throws LibvirtException { + if (isCentosHost()) { + /*define a volume, then override the file*/ + + StorageVol vol = destPool.storageVolCreateXML(destVol.toString(), 0); + String srcPath = srcVol.getKey(); + String destPath = vol.getKey(); + Script.runSimpleBashScript("cp " + srcPath + " " + destPath ); + return vol; + } else { + return destPool.storageVolCreateXMLFrom(destVol.toString(), srcVol, 0); + } + } + + private StorageVol getVolume(StoragePool pool, String volKey) { + StorageVol vol = null; + try { + vol = _conn.storageVolLookupByKey(volKey); + } catch (LibvirtException e) { + + } + if (vol == null) { + try { + pool.refresh(0); + } catch (LibvirtException e) { + + } + try { + vol = _conn.storageVolLookupByKey(volKey); + } catch (LibvirtException e) { + + } + } + return vol; + } + + private StorageVol getVolume(String volKey) throws LibvirtException{ + StorageVol vol = null; + + try { + vol = _conn.storageVolLookupByKey(volKey); + } catch (LibvirtException e) { + + } + if (vol == null) { + StoragePool pool = null; + String token[] = volKey.split("/"); + if (token.length <= 2) { + s_logger.debug("what the heck of volkey: " + volKey); + return null; + } + String poolUUID = token[token.length - 2]; + pool = _conn.storagePoolLookupByUUIDString(poolUUID); + pool.refresh(0); + vol = _conn.storageVolLookupByKey(volKey); + + } + return vol; + } + private String getPathOfStoragePool(StoragePool pool) throws LibvirtException { + return _mountPoint + File.separator + pool.getUUIDString() + File.separator; + } +} diff --git a/agent/src/com/cloud/agent/resource/computing/LibvirtDomainXMLParser.java b/agent/src/com/cloud/agent/resource/computing/LibvirtDomainXMLParser.java new file mode 100644 index 00000000000..779962af0a4 --- /dev/null +++ b/agent/src/com/cloud/agent/resource/computing/LibvirtDomainXMLParser.java @@ -0,0 +1,178 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.resource.computing; + +import java.util.ArrayList; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +/** + * @author chiradeep + * + */ +public class LibvirtDomainXMLParser extends LibvirtXMLParser { + + private final ArrayList interfaces = new ArrayList(); + private final SortedMap diskMaps = new TreeMap(); + + private boolean _interface; + private boolean _disk; + private boolean _desc; + private Integer vncPort; + private String diskDev; + private String diskFile; + private String desc; + + public Integer getVncPort() { + return vncPort; + } + + public void characters(char[] ch, int start, int length) throws SAXException { + if (_desc) { + desc = new String(ch, start, length); + } + } + + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws SAXException { + if(qName.equalsIgnoreCase("interface")) { + _interface = true; + } else if (qName.equalsIgnoreCase("target")){ + if (_interface) + interfaces.add(attributes.getValue("dev")); + else if (_disk) + diskDev = attributes.getValue("dev"); + } else if (qName.equalsIgnoreCase("source")){ + if (_disk) + diskFile = attributes.getValue("file"); + } else if (qName.equalsIgnoreCase("disk")) { + _disk = true; + } else if (qName.equalsIgnoreCase("graphics")) { + String port = attributes.getValue("port"); + if (port != null) { + try { + vncPort = Integer.parseInt(port); + if (vncPort != -1) { + vncPort = vncPort - 5900; + } else { + vncPort = null; + } + }catch (NumberFormatException nfe){ + vncPort = null; + } + } + } else if (qName.equalsIgnoreCase("description")) { + _desc = true; + } + } + + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + + if(qName.equalsIgnoreCase("interface")) { + _interface = false; + } else if (qName.equalsIgnoreCase("disk")) { + diskMaps.put(diskDev, diskFile); + _disk = false; + } else if (qName.equalsIgnoreCase("description")) { + _desc = false; + } + + } + + public ArrayList getInterfaces() { + return interfaces; + } + + public SortedMap getDiskMaps() { + return diskMaps; + } + + public String getDescription() { + return desc; + } + + public static void main(String [] args){ + LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + parser.parseDomainXML(""+ + "r-6-CV-5002-1"+ + "581b5a4b-b496-8d4d-e44e-a7dcbe9df0b5"+ + "testVM"+ + "131072"+ + "131072"+ + "1"+ + ""+ + "hvm"+ + "/var/lib/libvirt/qemu/vmlinuz-2.6.31.6-166.fc12.i686"+ + "ro root=/dev/sda1 acpi=force selinux=0 eth0ip=10.1.1.1 eth0mask=255.255.255.0 eth2ip=192.168.10.152 eth2mask=255.255.255.0 gateway=192.168.10.1 dns1=72.52.126.11 dns2=72.52.126.12 domain=v4.myvm.com"+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + "destroy"+ + "restart"+ + "destroy"+ + ""+ + "/usr/bin/qemu-kvm"+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + "" + + ); + for (String intf: parser.getInterfaces()){ + System.out.println(intf); + } + System.out.println(parser.getVncPort()); + System.out.println(parser.getDescription()); + } + +} diff --git a/agent/src/com/cloud/agent/resource/computing/LibvirtNetworkDef.java b/agent/src/com/cloud/agent/resource/computing/LibvirtNetworkDef.java new file mode 100644 index 00000000000..c4b5a53488a --- /dev/null +++ b/agent/src/com/cloud/agent/resource/computing/LibvirtNetworkDef.java @@ -0,0 +1,173 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.resource.computing; + +import java.util.ArrayList; +import java.util.List; + +public class LibvirtNetworkDef { + enum netType { + BRIDGE, + NAT, + LOCAL + } + private final String _networkName; + private final String _uuid; + private netType _networkType; + private String _brName; + private boolean _stp; + private int _delay; + private String _fwDev; + private final String _domainName; + private String _brIPAddr; + private String _brNetMask; + private final List ipranges = new ArrayList(); + private final List dhcpMaps = new ArrayList(); + public static class dhcpMapping { + String _mac; + String _name; + String _ip; + public dhcpMapping(String mac, String name, String ip) { + _mac = mac; + _name = name; + _ip = ip; + } + } + public static class IPRange { + String _start; + String _end; + public IPRange(String start, String end) { + _start = start; + _end = end; + } + } + + public LibvirtNetworkDef(String netName, String uuid, String domName) { + _networkName = netName; + _uuid = uuid; + _domainName = domName; + } + public void defNATNetwork(String brName, boolean stp, int delay, String fwNic, String ipAddr, String netMask) { + _networkType = netType.NAT; + _brName = brName; + _stp = stp; + _delay = delay; + _fwDev = fwNic; + _brIPAddr = ipAddr; + _brNetMask = netMask; + } + public void defBrNetwork(String brName, boolean stp, int delay, String fwNic, String ipAddr, String netMask) { + _networkType = netType.BRIDGE; + _brName = brName; + _stp = stp; + _delay = delay; + _fwDev = fwNic; + _brIPAddr = ipAddr; + _brNetMask = netMask; + } + public void defLocalNetwork(String brName, boolean stp, int delay, String ipAddr, String netMask) { + _networkType = netType.LOCAL; + _brName = brName; + _stp = stp; + _delay = delay; + _brIPAddr = ipAddr; + _brNetMask = netMask; + } + public void adddhcpIPRange(String start, String end) { + IPRange ipr = new IPRange(start, end); + ipranges.add(ipr); + } + public void adddhcpMapping(String mac, String host, String ip) { + dhcpMapping map = new dhcpMapping(mac, host, ip); + dhcpMaps.add(map); + } + @Override + public String toString() { + StringBuilder netBuilder = new StringBuilder(); + netBuilder.append("\n"); + netBuilder.append("" + _networkName + "\n"); + if (_uuid != null) + netBuilder.append("" + _uuid + "\n"); + if (_brName != null) { + netBuilder.append("\n"); + } + if (_domainName != null) { + netBuilder.append("\n"); + } + if (_networkType == netType.BRIDGE) { + netBuilder.append("\n"); + } else if (_networkType == netType.NAT) { + netBuilder.append("\n"); + } + if (_brIPAddr != null || _brNetMask != null || !ipranges.isEmpty() || !dhcpMaps.isEmpty()) { + netBuilder.append("\n"); + + if (!ipranges.isEmpty() || !dhcpMaps.isEmpty()) { + netBuilder.append("\n"); + for (IPRange ip : ipranges ) { + netBuilder.append("\n"); + } + for (dhcpMapping map : dhcpMaps) { + netBuilder.append("\n"); + } + netBuilder.append("\n"); + } + netBuilder.append("\n"); + } + netBuilder.append("\n"); + return netBuilder.toString(); + } + /** + * @param args + */ + public static void main(String[] args) { + LibvirtNetworkDef net = new LibvirtNetworkDef("cloudPrivate", null, "cloud.com"); + net.defNATNetwork("cloudbr0", false, 0, null, "192.168.168.1", "255.255.255.0"); + net.adddhcpIPRange("192.168.168.100", "192.168.168.220"); + net.adddhcpIPRange("192.168.168.10", "192.168.168.50"); + net.adddhcpMapping("branch0.cloud.com", "00:16:3e:77:e2:ed", "192.168.168.100"); + net.adddhcpMapping("branch1.cloud.com", "00:16:3e:77:e2:ef", "192.168.168.101"); + net.adddhcpMapping("branch2.cloud.com", "00:16:3e:77:e2:f0", "192.168.168.102"); + System.out.println(net.toString()); + + } + +} diff --git a/agent/src/com/cloud/agent/resource/computing/LibvirtStoragePoolDef.java b/agent/src/com/cloud/agent/resource/computing/LibvirtStoragePoolDef.java new file mode 100644 index 00000000000..5b86525a932 --- /dev/null +++ b/agent/src/com/cloud/agent/resource/computing/LibvirtStoragePoolDef.java @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.resource.computing; + +public class LibvirtStoragePoolDef { + public enum poolType { + ISCSI("iscsi"), + NFS("netfs"), + DIR("dir"); + String _poolType; + poolType(String poolType) { + _poolType = poolType; + } + @Override + public String toString() { + return _poolType; + } + } + private poolType _poolType; + private String _poolName; + private String _uuid; + private String _sourceHost; + private String _sourceDir; + private String _targetPath; + + public LibvirtStoragePoolDef(poolType type, String poolName, String uuid, String host, String dir, String targetPath) { + _poolType = type; + _poolName = poolName; + _uuid = uuid; + _sourceHost = host; + _sourceDir = dir; + _targetPath = targetPath; + } + + @Override + public String toString() { + StringBuilder storagePoolBuilder = new StringBuilder(); + storagePoolBuilder.append("\n"); + storagePoolBuilder.append("" + _poolName + "\n"); + if (_uuid != null) + storagePoolBuilder.append("" + _uuid + "\n"); + if (_poolType == poolType.NFS) { + storagePoolBuilder.append("\n"); + storagePoolBuilder.append("\n"); + storagePoolBuilder.append("\n"); + storagePoolBuilder.append("\n"); + } + storagePoolBuilder.append("\n"); + storagePoolBuilder.append("" + _targetPath + "\n"); + storagePoolBuilder.append("\n"); + storagePoolBuilder.append("\n"); + return storagePoolBuilder.toString(); + } +} diff --git a/agent/src/com/cloud/agent/resource/computing/LibvirtStorageVolumeDef.java b/agent/src/com/cloud/agent/resource/computing/LibvirtStorageVolumeDef.java new file mode 100644 index 00000000000..caa71614b9a --- /dev/null +++ b/agent/src/com/cloud/agent/resource/computing/LibvirtStorageVolumeDef.java @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.resource.computing; + +public class LibvirtStorageVolumeDef { + public enum volFormat { + RAW("raw"), + QCOW2("qcow2"), + DIR("dir"); + private String _format; + volFormat(String format) { + _format = format; + } + @Override + public String toString() { + return _format; + } + } + private String _volName; + private Long _volSize; + private volFormat _volFormat; + private String _backingPath; + private volFormat _backingFormat; + + public LibvirtStorageVolumeDef(String volName, Long size, volFormat format, String tmplPath, volFormat tmplFormat) { + _volName = volName; + _volSize = size; + _volFormat = format; + _backingPath = tmplPath; + _backingFormat = tmplFormat; + } + + @Override + public String toString() { + StringBuilder storageVolBuilder = new StringBuilder(); + storageVolBuilder.append("\n"); + storageVolBuilder.append("" + _volName + "\n"); + if (_volSize != null) { + storageVolBuilder.append("" + _volSize + "\n"); + } + storageVolBuilder.append("\n"); + storageVolBuilder.append("\n"); + storageVolBuilder.append("\n"); + if (_backingPath != null) { + storageVolBuilder.append("\n"); + storageVolBuilder.append("" + _backingPath + "\n"); + storageVolBuilder.append("\n"); + storageVolBuilder.append("\n"); + } + storageVolBuilder.append("\n"); + return storageVolBuilder.toString(); + } + +} diff --git a/agent/src/com/cloud/agent/resource/computing/LibvirtVMDef.java b/agent/src/com/cloud/agent/resource/computing/LibvirtVMDef.java new file mode 100644 index 00000000000..4d0ab0ccbcc --- /dev/null +++ b/agent/src/com/cloud/agent/resource/computing/LibvirtVMDef.java @@ -0,0 +1,653 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.resource.computing; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class LibvirtVMDef { + private String _hvsType; + private String _domName; + private String _domUUID; + private String _desc; + private final List components = new ArrayList(); + + public static class guestDef { + enum guestType { + KVM, + XEN, + EXE + } + enum bootOrder { + HARDISK("hd"), + CDROM("cdrom"), + FLOOPY("fd"), + NETWORK("network"); + String _order; + bootOrder(String order) { + _order = order; + } + @Override + public String toString() { + return _order; + } + } + private guestType _type; + private String _arch; + private String _loader; + private String _kernel; + private String _initrd; + private String _root; + private String _cmdline; + private List _bootdevs = new ArrayList(); + private String _machine; + public void setGuestType (guestType type) { + _type = type; + } + public void setGuestArch (String arch) { + _arch = arch; + } + public void setMachineType (String machine) { + _machine = machine; + } + public void setLoader (String loader) { + _loader = loader; + } + public void setBootKernel(String kernel, String initrd, String rootdev, String cmdline) { + _kernel = kernel; + _initrd = initrd; + _root = rootdev; + _cmdline = cmdline; + } + public void setBootOrder(bootOrder order) { + _bootdevs.add(order); + } + @Override + public String toString () { + if (_type == guestType.KVM) { + StringBuilder guestDef = new StringBuilder(); + guestDef.append("\n"); + guestDef.append("hvm\n"); + if (!_bootdevs.isEmpty()) { + for (bootOrder bo : _bootdevs) { + guestDef.append("\n"); + } + } + guestDef.append("\n"); + return guestDef.toString(); + } else + return null; + } + } + + public static class guestResourceDef { + private int _mem; + private int _currentMem = -1; + private String _memBacking; + private int _vcpu = -1; + public void setMemorySize(int mem) { + _mem = mem; + } + public void setCurrentMem(int currMem) { + _currentMem = currMem; + } + public void setMemBacking(String memBacking) { + _memBacking = memBacking; + } + public void setVcpuNum(int vcpu) { + _vcpu = vcpu; + } + @Override + public String toString(){ + StringBuilder resBuidler = new StringBuilder(); + resBuidler.append("" + _mem + "\n"); + if (_currentMem != -1) { + resBuidler.append("" + _currentMem + "\n"); + } + if (_memBacking != null) { + resBuidler.append("" + "<" + _memBacking + "/>" + "\n"); + } + if (_vcpu != -1) { + resBuidler.append("" + _vcpu + "\n"); + } + return resBuidler.toString(); + } + } + + public static class featuresDef { + private final List _features = new ArrayList(); + public void addFeatures(String feature) { + _features.add(feature); + } + @Override + public String toString() { + StringBuilder feaBuilder = new StringBuilder(); + feaBuilder.append("\n"); + for (String feature : _features) { + feaBuilder.append("<" + feature + "/>\n"); + } + feaBuilder.append("\n"); + return feaBuilder.toString(); + } + } + public static class termPolicy { + private String _reboot; + private String _powerOff; + private String _crash; + public termPolicy() { + _reboot = _powerOff = _crash = "destroy"; + } + public void setRebootPolicy(String rbPolicy) { + _reboot = rbPolicy; + } + public void setPowerOffPolicy(String poPolicy) { + _powerOff = poPolicy; + } + public void setCrashPolicy(String crashPolicy) { + _crash = crashPolicy; + } + @Override + public String toString() { + StringBuilder term = new StringBuilder(); + term.append("" + _reboot + "\n"); + term.append("" + _powerOff + "\n"); + term.append("" + _powerOff + "\n"); + return term.toString(); + } + } + + public static class devicesDef { + private String _emulator; + private final List devices = new ArrayList(); + public boolean addDevice(Object device) { + return devices.add(device); + } + public void setEmulatorPath(String emulator) { + _emulator = emulator; + } + @Override + public String toString() { + StringBuilder devicesBuilder = new StringBuilder(); + devicesBuilder.append("\n"); + if (_emulator != null) { + devicesBuilder.append("" + _emulator + "\n"); + } + for (Object o : devices) { + devicesBuilder.append(o.toString()); + } + devicesBuilder.append("\n"); + return devicesBuilder.toString(); + } + + } + public static class diskDef { + enum deviceType { + FLOOPY("floopy"), + DISK("disk"), + CDROM("cdrom"); + String _type; + deviceType(String type) { + _type = type; + } + @Override + public String toString() { + return _type; + } + } + enum diskType { + FILE("file"), + BLOCK("block"), + DIRECTROY("dir"); + String _diskType; + diskType(String type) { + _diskType = type; + } + @Override + public String toString() { + return _diskType; + } + } + enum diskBus { + IDE("ide"), + SCSI("scsi"), + VIRTIO("virtio"), + XEN("xen"), + USB("usb"), + UML("uml"), + FDC("fdc"); + String _bus; + diskBus(String bus) { + _bus = bus; + } + @Override + public String toString() { + return _bus; + } + } + enum diskFmtType { + RAW("raw"), + QCOW2("qcow2"); + String _fmtType; + diskFmtType(String fmt) { + _fmtType = fmt; + } + @Override + public String toString() { + return _fmtType; + } + } + + private deviceType _deviceType; /*floppy, disk, cdrom*/ + private diskType _diskType; + private String _sourcePath; + private String _diskLabel; + private diskBus _bus; + private diskFmtType _diskFmtType; /*qcow2, raw etc.*/ + private boolean _readonly = false; + private boolean _shareable = false; + private boolean _deferAttach = false; + public void setDeviceType(deviceType deviceType) { + _deviceType = deviceType; + } + public void defFileBasedDisk(String filePath, String diskLabel, diskBus bus, diskFmtType diskFmtType) { + _diskType = diskType.FILE; + _deviceType = deviceType.DISK; + _sourcePath = filePath; + _diskLabel = diskLabel; + _diskFmtType = diskFmtType; + _bus = bus; + + } + public void defBlockBasedDisk(String diskName, String diskLabel, diskBus bus) { + _diskType = diskType.BLOCK; + _deviceType = deviceType.DISK; + _sourcePath = diskName; + _diskLabel = diskLabel; + _bus = bus; + } + public void setReadonly() { + _readonly = true; + } + public void setSharable() { + _shareable = true; + } + public void setAttachDeferred(boolean deferAttach) { + _deferAttach = deferAttach; + } + public boolean isAttachDeferred() { + return _deferAttach; + } + public String getDiskPath() { + return _sourcePath; + } + public String getDiskLabel() { + return _diskLabel; + } + @Override + public String toString() { + StringBuilder diskBuilder = new StringBuilder(); + diskBuilder.append("\n"); + diskBuilder.append("\n"); + if (_diskType == diskType.FILE) { + diskBuilder.append("\n"); + } else if (_diskType == diskType.BLOCK) { + diskBuilder.append("\n"); + } + diskBuilder.append("\n"); + diskBuilder.append("\n"); + return diskBuilder.toString(); + } + } + + public static class interfaceDef { + enum guestNetType { + BRIDGE("bridge"), + NETWORK("network"), + USER("user"), + ETHERNET("ethernet"), + INTERNAL("internal"); + String _type; + guestNetType(String type) { + _type = type; + } + @Override + public String toString() { + return _type; + } + } + enum nicModel { + E1000("e1000"), + VIRTIO("virtio"), + RTL8139("rtl8139"), + NE2KPCI("ne2k_pci"); + String _model; + nicModel(String model) { + _model = model; + } + @Override + public String toString() { + return _model; + } + } + enum hostNicType { + DIRECT_ATTACHED_WITHOUT_DHCP, + DIRECT_ATTACHED_WITH_DHCP, + VNET, + VLAN; + } + private guestNetType _netType; /*bridge, ethernet, network, user, internal*/ + private hostNicType _hostNetType; /*Only used by agent java code*/ + private String _sourceName; + private String _networkName; + private String _macAddr; + private String _ipAddr; + private String _scriptPath; + private nicModel _model; + public void defBridgeNet(String brName, String targetBrName, String macAddr, nicModel model) { + _netType = guestNetType.BRIDGE; + _sourceName = brName; + _networkName = targetBrName; + _macAddr = macAddr; + _model = model; + } + public void defPrivateNet(String networkName, String targetName, String macAddr, nicModel model) { + _netType = guestNetType.NETWORK; + _sourceName = networkName; + _networkName = targetName; + _macAddr = macAddr; + _model = model; + } + + public void setHostNetType(hostNicType hostNetType) { + _hostNetType = hostNetType; + } + + public hostNicType getHostNetType() { + return _hostNetType; + } + + public String getBrName() { + return _sourceName; + } + public guestNetType getNetType() { + return _netType; + } + @Override + public String toString() { + StringBuilder netBuilder = new StringBuilder(); + netBuilder.append("\n"); + if (_netType == guestNetType.BRIDGE) { + netBuilder.append("\n"); + } else if (_netType == guestNetType.NETWORK) { + netBuilder.append("\n"); + } + if (_networkName !=null) { + netBuilder.append("\n"); + } + if (_macAddr !=null) { + netBuilder.append("\n"); + } + if (_model !=null) { + netBuilder.append("\n"); + } + netBuilder.append("\n"); + return netBuilder.toString(); + } + } + public static class consoleDef { + private final String _ttyPath; + private final String _type; + private final String _source; + private short _port = -1; + public consoleDef(String type, String path, String source, short port) { + _type = type; + _ttyPath = path; + _source = source; + _port = port; + } + @Override + public String toString() { + StringBuilder consoleBuilder = new StringBuilder(); + consoleBuilder.append("\n"); + if (_source != null) { + consoleBuilder.append("\n"); + } + if (_port != -1) { + consoleBuilder.append("\n"); + } + consoleBuilder.append("\n"); + return consoleBuilder.toString(); + } + } + public static class serialDef { + private final String _type; + private final String _source; + private short _port = -1; + public serialDef(String type, String source, short port) { + _type = type; + _source = source; + _port = port; + } + @Override + public String toString() { + StringBuilder serialBuidler = new StringBuilder(); + serialBuidler.append("\n"); + if (_source != null) { + serialBuidler.append("\n"); + } + if (_port != -1) { + serialBuidler.append("\n"); + } + serialBuidler.append("\n"); + return serialBuidler.toString(); + } + } + public static class graphicDef { + private final String _type; + private short _port = -2; + private boolean _autoPort = false; + private final String _listenAddr; + private final String _passwd; + private final String _keyMap; + public graphicDef(String type, short port, boolean auotPort, String listenAddr, String passwd, String keyMap) { + _type = type; + _port = port; + _autoPort = auotPort; + _listenAddr = listenAddr; + _passwd = passwd; + _keyMap = keyMap; + } + @Override + public String toString() { + StringBuilder graphicBuilder = new StringBuilder(); + graphicBuilder.append("\n"); + return graphicBuilder.toString(); + } + } + public static class inputDef { + private final String _type; /*tablet, mouse*/ + private final String _bus; /*ps2, usb, xen*/ + public inputDef(String type, String bus) { + _type = type; + _bus = bus; + } + @Override + public String toString() { + StringBuilder inputBuilder = new StringBuilder(); + inputBuilder.append("\n"); + return inputBuilder.toString(); + } + } + public void setHvsType(String hvs) { + _hvsType = hvs; + } + public void setDomainName(String domainName) { + _domName = domainName; + } + public void setDomUUID(String uuid) { + _domUUID = uuid; + } + public void setDomDescription(String desc) { + _desc = desc; + } + public boolean addComp(Object comp) { + return components.add(comp); + } + @Override + public String toString() { + StringBuilder vmBuilder = new StringBuilder(); + vmBuilder.append("\n"); + vmBuilder.append("" + _domName + "\n"); + if (_domUUID != null) { + vmBuilder.append("" + _domUUID + "\n"); + } + if (_desc != null ) { + vmBuilder.append("" + _desc + "\n"); + } + for (Object o : components) { + vmBuilder.append(o.toString()); + } + vmBuilder.append("\n"); + return vmBuilder.toString(); + } + + public static void main(String [] args){ + System.out.println("testing"); + LibvirtVMDef vm = new LibvirtVMDef(); + vm.setHvsType("kvm"); + vm.setDomainName("testing"); + vm.setDomUUID(UUID.randomUUID().toString()); + + guestDef guest = new guestDef(); + guest.setGuestType(guestDef.guestType.KVM); + guest.setGuestArch("x86_64"); + guest.setMachineType("pc-0.11"); + guest.setBootOrder(guestDef.bootOrder.HARDISK); + vm.addComp(guest); + + guestResourceDef grd = new guestResourceDef(); + grd.setMemorySize(512*1024); + grd.setVcpuNum(1); + vm.addComp(grd); + + featuresDef features = new featuresDef(); + features.addFeatures("pae"); + features.addFeatures("apic"); + features.addFeatures("acpi"); + vm.addComp(features); + + termPolicy term = new termPolicy(); + term.setCrashPolicy("destroy"); + term.setPowerOffPolicy("destroy"); + term.setRebootPolicy("destroy"); + vm.addComp(term); + + devicesDef devices = new devicesDef(); + devices.setEmulatorPath("/usr/bin/cloud-qemu-system-x86_64"); + + diskDef hda = new diskDef(); + hda.defFileBasedDisk("/path/to/hda1", "hda", diskDef.diskBus.IDE, diskDef.diskFmtType.QCOW2); + devices.addDevice(hda); + + diskDef hdb = new diskDef(); + hdb.defFileBasedDisk("/path/to/hda2", "hdb", diskDef.diskBus.IDE, diskDef.diskFmtType.QCOW2); + devices.addDevice(hdb); + + interfaceDef pubNic = new interfaceDef(); + pubNic.defBridgeNet("cloudbr0", "vnet1", "00:16:3e:77:e2:a1", interfaceDef.nicModel.VIRTIO); + devices.addDevice(pubNic); + + interfaceDef privNic = new interfaceDef(); + privNic.defPrivateNet("cloud-private", null, "00:16:3e:77:e2:a2", interfaceDef.nicModel.VIRTIO); + devices.addDevice(privNic); + + interfaceDef vlanNic = new interfaceDef(); + vlanNic.defBridgeNet("vnbr1000", "tap1", "00:16:3e:77:e2:a2", interfaceDef.nicModel.VIRTIO); + devices.addDevice(vlanNic); + + serialDef serial = new serialDef("pty", null, (short)0); + devices.addDevice(serial); + + consoleDef console = new consoleDef("pty", null, null, (short)0); + devices.addDevice(console); + + graphicDef grap = new graphicDef("vnc", (short)0, true, null, null, null); + devices.addDevice(grap); + + inputDef input = new inputDef("tablet", "usb"); + devices.addDevice(input); + + vm.addComp(devices); + + System.out.println(vm.toString()); + } + +} diff --git a/agent/src/com/cloud/agent/resource/computing/LibvirtXMLParser.java b/agent/src/com/cloud/agent/resource/computing/LibvirtXMLParser.java new file mode 100644 index 00000000000..b954a5f2559 --- /dev/null +++ b/agent/src/com/cloud/agent/resource/computing/LibvirtXMLParser.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.resource.computing; + +import java.io.IOException; +import java.io.StringReader; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.log4j.Logger; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +public class LibvirtXMLParser extends DefaultHandler{ + private static final Logger s_logger = Logger.getLogger(LibvirtXMLParser.class); + protected static SAXParserFactory s_spf; + + static { + s_spf = SAXParserFactory.newInstance(); + + } + protected SAXParser _sp; + protected boolean _initialized = false; + + + public LibvirtXMLParser(){ + + try { + _sp = s_spf.newSAXParser(); + _initialized = true; + } catch(Exception ex) { + } + + } + + public boolean parseDomainXML(String domXML) { + if (!_initialized){ + return false; + } + try { + _sp.parse(new InputSource(new StringReader(domXML)), this); + return true; + }catch(SAXException se) { + s_logger.warn(se.getMessage()); + }catch (IOException ie) { + s_logger.error(ie.getMessage()); + } + return false; + } + + + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + } + + + + + + +} + + + + + diff --git a/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java b/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java new file mode 100644 index 00000000000..4696b98b426 --- /dev/null +++ b/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java @@ -0,0 +1,353 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.resource.consoleproxy; + + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLConnection; +import java.util.Map; +import java.util.Properties; + +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.agent.Agent.ExitStatus; +import com.cloud.agent.api.AgentControlAnswer; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckHealthAnswer; +import com.cloud.agent.api.CheckHealthCommand; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.ConsoleAccessAuthenticationAnswer; +import com.cloud.agent.api.ConsoleAccessAuthenticationCommand; +import com.cloud.agent.api.ConsoleProxyLoadReportCommand; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.ReadyAnswer; +import com.cloud.agent.api.ReadyCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.agent.api.StartupProxyCommand; +import com.cloud.agent.api.proxy.CheckConsoleProxyLoadCommand; +import com.cloud.agent.api.proxy.ConsoleProxyLoadAnswer; +import com.cloud.agent.api.proxy.WatchConsoleProxyLoadCommand; +import com.cloud.exception.AgentControlChannelException; +import com.cloud.host.Host; +import com.cloud.host.Host.Type; +import com.cloud.resource.ServerResource; +import com.cloud.resource.ServerResourceBase; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.net.NetUtils; +import com.cloud.utils.script.Script; + +/** + * + * I don't want to introduce extra cross-cutting concerns into console proxy process, as it involves configurations like + * zone/pod, agent auto self-upgrade etc. I also don't want to introduce more module dependency issues into our build system, + * cross-communication between this resource and console proxy will be done through reflection. As a result, come out with + * following solution to solve the problem of building a communication channel between consoole proxy and management server. + * + * We will deploy an agent shell inside console proxy VM, and this agent shell will launch current console proxy from within + * this special server resource, through it console proxy can build a communication channel with management server. + * + */ +public class ConsoleProxyResource extends ServerResourceBase implements ServerResource { + static final Logger s_logger = Logger.getLogger(ConsoleProxyResource.class); + + private final Properties _properties = new Properties(); + private Thread _consoleProxyMain = null; + + long _proxyVmId; + int _proxyPort; + + String _localgw; + String _eth1ip; + String _eth1mask; + + @Override + public Answer executeRequest(final Command cmd) { + if (cmd instanceof CheckConsoleProxyLoadCommand) { + return execute((CheckConsoleProxyLoadCommand)cmd); + } else if(cmd instanceof WatchConsoleProxyLoadCommand) { + return execute((WatchConsoleProxyLoadCommand)cmd); + } else if (cmd instanceof ReadyCommand) { + return new ReadyAnswer((ReadyCommand)cmd); + } else if(cmd instanceof CheckHealthCommand) { + return new CheckHealthAnswer((CheckHealthCommand)cmd, true); + } else { + return Answer.createUnsupportedCommandAnswer(cmd); + } + } + + protected Answer execute(final CheckConsoleProxyLoadCommand cmd) { + return executeProxyLoadScan(cmd, cmd.getProxyVmId(), cmd.getProxyVmName(), cmd.getProxyManagementIp(), cmd.getProxyCmdPort()); + } + + protected Answer execute(final WatchConsoleProxyLoadCommand cmd) { + return executeProxyLoadScan(cmd, cmd.getProxyVmId(), cmd.getProxyVmName(), cmd.getProxyManagementIp(), cmd.getProxyCmdPort()); + } + + private Answer executeProxyLoadScan(final Command cmd, final long proxyVmId, final String proxyVmName, final String proxyManagementIp, final int cmdPort) { + String result = null; + + final StringBuffer sb = new StringBuffer(); + sb.append("http://").append(proxyManagementIp).append(":" + cmdPort).append("/cmd/getstatus"); + + boolean success = true; + try { + final URL url = new URL(sb.toString()); + final URLConnection conn = url.openConnection(); + + final InputStream is = conn.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + final StringBuilder sb2 = new StringBuilder(); + String line = null; + try { + while ((line = reader.readLine()) != null) + sb2.append(line + "\n"); + result = sb2.toString(); + } catch (final IOException e) { + success = false; + } finally { + try { + is.close(); + } catch (final IOException e) { + s_logger.warn("Exception when closing , console proxy address : " + proxyManagementIp); + success = false; + } + } + } catch(final IOException e) { + s_logger.warn("Unable to open console proxy command port url, console proxy address : " + proxyManagementIp); + success = false; + } + + return new ConsoleProxyLoadAnswer(cmd, proxyVmId, proxyVmName, success, result); + } + + @Override + protected String getDefaultScriptsDir() { + return null; + } + + public Type getType() { + return Host.Type.ConsoleProxy; + } + + @Override + public synchronized StartupCommand [] initialize() { + final StartupProxyCommand cmd = new StartupProxyCommand(); + fillNetworkInformation(cmd); + cmd.setProxyPort(_proxyPort); + cmd.setProxyVmId(_proxyVmId); + return new StartupCommand[] {cmd}; + } + + @Override + public void disconnected() { + } + + @Override + public PingCommand getCurrentStatus(long id) { + return new PingCommand(Type.ConsoleProxy, id); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + _localgw = (String)params.get("localgw"); + _eth1mask = (String)params.get("eth1mask"); + _eth1ip = (String)params.get("eth1ip"); + if (_eth1ip != null) { + params.put("private.network.device", "eth1"); + } else { + s_logger.warn("WARNING: eth1ip parameter is not found!"); + } + + String eth2ip = (String) params.get("eth2ip"); + if (eth2ip != null) { + params.put("public.network.device", "eth2"); + } else { + s_logger.warn("WARNING: eth2ip parameter is not found!"); + } + + super.configure(name, params); + + for(Map.Entry entry : params.entrySet()) { + _properties.put(entry.getKey(), entry.getValue()); + } + + String value = (String)params.get("premium"); + if(value != null && value.equals("premium")) + _proxyPort = 443; + else { + value = (String)params.get("consoleproxy.httpListenPort"); + _proxyPort = NumbersUtil.parseInt(value, 80); + } + + value = (String)params.get("proxy_vm"); + _proxyVmId = NumbersUtil.parseLong(value, 0); + + if (_localgw != null) { + String internalDns1 = (String)params.get("dns1"); + String internalDns2 = (String)params.get("dns2"); + + if (internalDns1 == null) { + s_logger.warn("No DNS entry found during configuration of NfsSecondaryStorage"); + } else { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, internalDns1); + } + + String mgmtHost = (String)params.get("host"); + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + if (internalDns2 != null) { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, internalDns2); + } + } + + if(s_logger.isInfoEnabled()) + s_logger.info("Receive proxyVmId in ConsoleProxyResource configuration as " + _proxyVmId); + + launchConsoleProxy(); + return true; + } + + private void addRouteToInternalIpOrCidr(String localgw, String eth1ip, String eth1mask, String destIpOrCidr) { + s_logger.debug("addRouteToInternalIp: localgw=" + localgw + ", eth1ip=" + eth1ip + ", eth1mask=" + eth1mask + ",destIp=" + destIpOrCidr); + if (destIpOrCidr == null) { + s_logger.debug("addRouteToInternalIp: destIp is null"); + return; + } + if (!NetUtils.isValidIp(destIpOrCidr) && !NetUtils.isValidCIDR(destIpOrCidr)){ + s_logger.warn(" destIp is not a valid ip address or cidr destIp=" + destIpOrCidr); + return; + } + boolean inSameSubnet = false; + if (NetUtils.isValidIp(destIpOrCidr)) { + if (eth1ip != null && eth1mask != null) { + inSameSubnet = NetUtils.sameSubnet(eth1ip, destIpOrCidr, eth1mask); + } else { + s_logger.warn("addRouteToInternalIp: unable to determine same subnet: _eth1ip=" + eth1ip + ", dest ip=" + destIpOrCidr + ", _eth1mask=" + eth1mask); + } + } else { + inSameSubnet = NetUtils.isNetworkAWithinNetworkB(destIpOrCidr, NetUtils.ipAndNetMaskToCidr(eth1ip, eth1mask)); + } + if (inSameSubnet) { + s_logger.debug("addRouteToInternalIp: dest ip " + destIpOrCidr + " is in the same subnet as eth1 ip " + eth1ip); + return; + } + Script command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add("ip route delete " + destIpOrCidr); + command.execute(); + command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add("ip route add " + destIpOrCidr + " via " + localgw); + String result = command.execute(); + if (result != null) { + s_logger.warn("Error in configuring route to internal ip err=" + result ); + } else { + s_logger.debug("addRouteToInternalIp: added route to internal ip=" + destIpOrCidr + " via " + localgw); + } + } + + @Override + public String getName() { + return _name; + } + + private void launchConsoleProxy() { + final Object resource = this; + + _consoleProxyMain = new Thread(new Runnable() { + public void run() { + try { + Class consoleProxyClazz = Class.forName("com.cloud.consoleproxy.ConsoleProxy"); + try { + Method method = consoleProxyClazz.getMethod("startWithContext", Properties.class, Object.class); + method.invoke(null, _properties, resource); + } catch (SecurityException e) { + s_logger.error("Unable to launch console proxy due to SecurityException"); + System.exit(ExitStatus.Error.value()); + } catch (NoSuchMethodException e) { + s_logger.error("Unable to launch console proxy due to NoSuchMethodException"); + System.exit(ExitStatus.Error.value()); + } catch (IllegalArgumentException e) { + s_logger.error("Unable to launch console proxy due to IllegalArgumentException"); + System.exit(ExitStatus.Error.value()); + } catch (IllegalAccessException e) { + s_logger.error("Unable to launch console proxy due to IllegalAccessException"); + System.exit(ExitStatus.Error.value()); + } catch (InvocationTargetException e) { + s_logger.error("Unable to launch console proxy due to InvocationTargetException"); + System.exit(ExitStatus.Error.value()); + } + } catch (final ClassNotFoundException e) { + s_logger.error("Unable to launch console proxy due to ClassNotFoundException"); + System.exit(ExitStatus.Error.value()); + } + } + }, "Console-Proxy-Main"); + _consoleProxyMain.setDaemon(true); + _consoleProxyMain.start(); + } + + public boolean authenticateConsoleAccess(String vmId, String sid) { + ConsoleAccessAuthenticationCommand cmd = new ConsoleAccessAuthenticationCommand(vmId, sid); + + try { + AgentControlAnswer answer = getAgentControl().sendRequest(cmd, 10000); + if(answer != null) { + return ((ConsoleAccessAuthenticationAnswer)answer).succeeded(); + } else { + s_logger.error("Authentication failed for vm: " + vmId + " with sid: " + sid); + } + + } catch (AgentControlChannelException e) { + s_logger.error("Unable to send out console access authentication request due to " + e.getMessage(), e); + } + + return false; + } + + public void reportLoadInfo(String gsonLoadInfo) { + ConsoleProxyLoadReportCommand cmd = new ConsoleProxyLoadReportCommand(_proxyVmId, gsonLoadInfo); + try { + getAgentControl().postRequest(cmd); + + if(s_logger.isDebugEnabled()) + s_logger.debug("Report proxy load info, proxy : " + _proxyVmId + ", load: " + gsonLoadInfo); + } catch (AgentControlChannelException e) { + s_logger.error("Unable to send out load info due to " + e.getMessage(), e); + } + } + + public void ensureRoute(String address) { + if(_localgw != null) { + if(s_logger.isDebugEnabled()) + s_logger.debug("Ensure route for " + address + " via " + _localgw); + + // this method won't be called in high frequency, serialize access to script execution + synchronized(this) { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, address); + } + } + } +} diff --git a/agent/src/com/cloud/agent/resource/storage/IscsiMountPreparer.java b/agent/src/com/cloud/agent/resource/storage/IscsiMountPreparer.java new file mode 100644 index 00000000000..97198e77c7c --- /dev/null +++ b/agent/src/com/cloud/agent/resource/storage/IscsiMountPreparer.java @@ -0,0 +1,224 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.resource.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.resource.DiskPreparer; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.VirtualMachineTemplate.BootloaderType; +import com.cloud.storage.Volume.VolumeType; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.script.Script; + +public class IscsiMountPreparer implements DiskPreparer { + private static final Logger s_logger = Logger.getLogger(IscsiMountPreparer.class); + + private String _name; + + private String _mountvmPath; + private String _mountRootdiskPath; + private String _mountDatadiskPath; + protected String _mountParent; + protected int _mountTimeout; + + @Override + public String mount(String vmName, VolumeVO vol, BootloaderType type) { + return null; + } + + @Override + public boolean unmount(String path) { + return false; + } + + protected static VolumeVO findVolume(final List volumes, final Volume.VolumeType vType) { + if (volumes == null) return null; + + for (final VolumeVO v: volumes) { + if (v.getVolumeType() == vType) + return v; + } + + return null; + } + + protected static List findVolumes(final List volumes, final Volume.VolumeType vType) { + if (volumes == null) return null; + final List result = new ArrayList(); + for (final VolumeVO v: volumes) { + if (v.getVolumeType() == vType) + result.add(v); + } + + return result; + } + + protected static VolumeVO findVolume(final List volumes, final Volume.VolumeType vType, final String storageHost) { + if (volumes == null) return null; + for (final VolumeVO v: volumes) { + if ((v.getVolumeType() == vType) && (v.getHostIp().equalsIgnoreCase(storageHost))) + return v; + } + return null; + } + + protected static boolean mirroredVolumes(final List vols, final Volume.VolumeType vType) { + final List volumes = findVolumes(vols, vType); + return volumes.size() > 1; + } + + public synchronized String mountImage(final String host, final String dest, final String vmName, final List volumes, final BootloaderType bootloader) { + final Script command = new Script(_mountvmPath, _mountTimeout, s_logger); + command.add("-h", host); + command.add("-l", dest); + command.add("-n", vmName); + command.add("-b", bootloader.toString()); + + command.add("-t"); + + + final VolumeVO root = findVolume(volumes, Volume.VolumeType.ROOT); + if (root == null) { + return null; + } + command.add(root.getIscsiName()); + command.add("-r", root.getFolder()); + + final VolumeVO swap = findVolume(volumes, Volume.VolumeType.SWAP); + if (swap !=null && swap.getIscsiName() != null) { + command.add("-w", swap.getIscsiName()); + } + + final VolumeVO datadsk = findVolume(volumes, Volume.VolumeType.DATADISK); + if (datadsk !=null && datadsk.getIscsiName() != null) { + command.add("-1", datadsk.getIscsiName()); + } + + return command.execute(); + } + + public synchronized String mountImage(final String storageHosts[], final String dest, final String vmName, final List volumes, final boolean mirroredVols, final BootloaderType booter) { + if (!mirroredVols) { + return mountImage(storageHosts[0], dest, vmName, volumes, booter); + } else { + return mountMirroredImage(storageHosts, dest, vmName, volumes, booter); + } + } + + protected String mountMirroredImage(final String hosts[], final String dest, final String vmName, final List volumes, final BootloaderType booter) { + final List rootDisks = findVolumes(volumes, VolumeType.ROOT); + final String storIp0 = hosts[0]; + final String storIp1 = hosts[1]; + //mountrootdisk.sh -m -h $STORAGE0 -t $iqn0 -l $src -n $vmname -r $dest -M -H $STORAGE1 -T $iqn1 + final Script command = new Script(_mountRootdiskPath, _mountTimeout, s_logger); + command.add("-m"); + command.add("-M"); + command.add("-h", storIp0); + command.add("-H", storIp1); + command.add("-l", dest); + command.add("-r", rootDisks.get(0).getFolder()); + command.add("-n", vmName); + command.add("-t", rootDisks.get(0).getIscsiName()); + command.add("-T", rootDisks.get(1).getIscsiName()); + command.add("-b", booter.toString()); + + final List swapDisks = findVolumes(volumes, VolumeType.SWAP); + if (swapDisks.size() == 2) { + command.add("-w", swapDisks.get(0).getIscsiName()); + command.add("-W", swapDisks.get(1).getIscsiName()); + } + + final String result = command.execute(); + if (result == null){ + final List dataDisks = findVolumes(volumes, VolumeType.DATADISK); + if (dataDisks.size() == 2) { + final Script mountdata = new Script(_mountDatadiskPath, _mountTimeout, s_logger); + mountdata.add("-m"); + mountdata.add("-M"); + mountdata.add("-h", storIp0); + mountdata.add("-H", storIp1); + mountdata.add("-n", vmName); + mountdata.add("-c", "1"); + mountdata.add("-d", dataDisks.get(0).getIscsiName()); + mountdata.add("-D", dataDisks.get(1).getIscsiName()); + return mountdata.execute(); + + } else if (dataDisks.size() == 0){ + return result; + } + } + + return result; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + _name = name; + + String scriptsDir = (String)params.get("mount.scripts.dir"); + if (scriptsDir == null) { + scriptsDir = "scripts/vm/storage/iscsi/comstar/filebacked"; + } + + _mountDatadiskPath = Script.findScript(scriptsDir, "mountdatadisk.sh"); + if (_mountDatadiskPath == null) { + throw new ConfigurationException("Unable to find mountdatadisk.sh"); + } + s_logger.info("mountdatadisk.sh found in " + _mountDatadiskPath); + + String value = (String)params.get("mount.script.timeout"); + _mountTimeout = NumbersUtil.parseInt(value, 240) * 1000; + + _mountvmPath = Script.findScript(scriptsDir, "mountvm.sh"); + if (_mountvmPath == null) { + throw new ConfigurationException("Unable to find mountvm.sh"); + } + s_logger.info("mountvm.sh found in " + _mountvmPath); + + _mountRootdiskPath = Script.findScript(scriptsDir, "mountrootdisk.sh"); + if (_mountRootdiskPath == null) { + throw new ConfigurationException("Unable to find mountrootdisk.sh"); + } + s_logger.info("mountrootdisk.sh found in " + _mountRootdiskPath); + + return true; + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } +} diff --git a/agent/test/com/cloud/agent/TestAgentShell.java b/agent/test/com/cloud/agent/TestAgentShell.java new file mode 100644 index 00000000000..a9e23641cfb --- /dev/null +++ b/agent/test/com/cloud/agent/TestAgentShell.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent; + +import java.io.File; +import java.io.IOException; + +import org.apache.log4j.Logger; + +import com.cloud.agent.AgentShell; +import com.cloud.utils.testcase.Log4jEnabledTestCase; + +public class TestAgentShell extends Log4jEnabledTestCase { + protected final static Logger s_logger = Logger.getLogger(TestAgentShell.class); + + public void testWget() { + File file = null; + try { + file = File.createTempFile("wget", ".html"); + AgentShell.wget("http://www.google.com/", file); + + if (s_logger.isDebugEnabled()) { + s_logger.debug("file saved to " + file.getAbsolutePath()); + } + + } catch (final IOException e) { + s_logger.warn("Exception while downloading agent update package, ", e); + } + } +} diff --git a/api/.classpath b/api/.classpath new file mode 100644 index 00000000000..1522af664ce --- /dev/null +++ b/api/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/api/.project b/api/.project new file mode 100644 index 00000000000..0df4729c250 --- /dev/null +++ b/api/.project @@ -0,0 +1,17 @@ + + + api + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/api/src/com/cloud/deploy/DataCenterDeployment.java b/api/src/com/cloud/deploy/DataCenterDeployment.java new file mode 100644 index 00000000000..ba8b227c179 --- /dev/null +++ b/api/src/com/cloud/deploy/DataCenterDeployment.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.deploy; + +public class DataCenterDeployment implements DeploymentStrategy { + long _dcId; + public DataCenterDeployment(long dataCenterId) { + _dcId = dataCenterId; + } + + public long getDataCenterId() { + return _dcId; + } +} diff --git a/api/src/com/cloud/deploy/DeploymentStrategy.java b/api/src/com/cloud/deploy/DeploymentStrategy.java new file mode 100644 index 00000000000..57bdeca6188 --- /dev/null +++ b/api/src/com/cloud/deploy/DeploymentStrategy.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.deploy; + +/** + * Describes how a VM should be deployed. + * + */ +public interface DeploymentStrategy { + +} diff --git a/api/src/com/cloud/exception/AgentUnavailableException.java b/api/src/com/cloud/exception/AgentUnavailableException.java new file mode 100644 index 00000000000..2e6632c58a3 --- /dev/null +++ b/api/src/com/cloud/exception/AgentUnavailableException.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +/** + * This exception is thrown when the agent is unavailable to accept an + * command. + * + */ +public class AgentUnavailableException extends Exception { + + private static final long serialVersionUID = SerialVersionUID.AgentUnavailableException; + + long _agentId; + + public AgentUnavailableException(String msg, long agentId) { + super("Host " + agentId + ": " + msg); + _agentId = agentId; + } + + public AgentUnavailableException(long agentId) { + this("Unable to reach host.", agentId); + } + + public long getAgentId() { + return _agentId; + } +} diff --git a/api/src/com/cloud/exception/ConcurrentOperationException.java b/api/src/com/cloud/exception/ConcurrentOperationException.java new file mode 100644 index 00000000000..43b85014443 --- /dev/null +++ b/api/src/com/cloud/exception/ConcurrentOperationException.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +public class ConcurrentOperationException extends Exception { + + private static final long serialVersionUID = SerialVersionUID.ConcurrentOperationException; + + public ConcurrentOperationException(String msg) { + super(msg); + } +} diff --git a/api/src/com/cloud/exception/DiscoveryException.java b/api/src/com/cloud/exception/DiscoveryException.java new file mode 100644 index 00000000000..c6481d828e4 --- /dev/null +++ b/api/src/com/cloud/exception/DiscoveryException.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +public class DiscoveryException extends Exception { + + private static final long serialVersionUID = SerialVersionUID.DiscoveryException; + + public DiscoveryException(String msg) { + this(msg, null); + } + + public DiscoveryException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/api/src/com/cloud/exception/HAStateException.java b/api/src/com/cloud/exception/HAStateException.java new file mode 100644 index 00000000000..3b1bf24ec99 --- /dev/null +++ b/api/src/com/cloud/exception/HAStateException.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +/** + * This exception is thrown when a machine is in HA State and a operation, + * such as start or stop, is attempted on it. Machines that are in HA + * states need to be properly cleaned up before anything special can be + * done with it. Hence this special state. + */ +public class HAStateException extends ManagementServerException { + + private static final long serialVersionUID = SerialVersionUID.HAStateException; + + public HAStateException(String msg) { + super(msg); + } +} diff --git a/api/src/com/cloud/exception/InsufficientAddressCapacityException.java b/api/src/com/cloud/exception/InsufficientAddressCapacityException.java new file mode 100644 index 00000000000..1cca265f1ec --- /dev/null +++ b/api/src/com/cloud/exception/InsufficientAddressCapacityException.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +/** + * Exception thrown when the end there's not enough ip addresses in the system. + */ +public class InsufficientAddressCapacityException extends InsufficientCapacityException { + + private static final long serialVersionUID = SerialVersionUID.InsufficientAddressCapacityException; + + public InsufficientAddressCapacityException(String msg) { + super(msg); + } + + protected InsufficientAddressCapacityException() { + super(); + } + +} diff --git a/api/src/com/cloud/exception/InsufficientCapacityException.java b/api/src/com/cloud/exception/InsufficientCapacityException.java new file mode 100755 index 00000000000..233306b6000 --- /dev/null +++ b/api/src/com/cloud/exception/InsufficientCapacityException.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +/** + * Generic parent exception class for capacity being reached. + * + */ +public abstract class InsufficientCapacityException extends Exception { + private static final long serialVersionUID = SerialVersionUID.InsufficientCapacityException; + + protected InsufficientCapacityException() { + } + + public InsufficientCapacityException(String msg) { + super(msg); + } +} diff --git a/api/src/com/cloud/exception/InsufficientStorageCapacityException.java b/api/src/com/cloud/exception/InsufficientStorageCapacityException.java new file mode 100755 index 00000000000..1f2b9c94977 --- /dev/null +++ b/api/src/com/cloud/exception/InsufficientStorageCapacityException.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +/** + * InsufficientStorageCapcityException is thrown when there's not enough + * storage space to create the VM. + */ +public class InsufficientStorageCapacityException extends InsufficientCapacityException { + + private static final long serialVersionUID = SerialVersionUID.InsufficientStorageCapacityException; + + public InsufficientStorageCapacityException(String msg) { + super(msg); + } +} diff --git a/api/src/com/cloud/exception/InsufficientVirtualNetworkCapcityException.java b/api/src/com/cloud/exception/InsufficientVirtualNetworkCapcityException.java new file mode 100644 index 00000000000..ded4f9fd966 --- /dev/null +++ b/api/src/com/cloud/exception/InsufficientVirtualNetworkCapcityException.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +public class InsufficientVirtualNetworkCapcityException extends InsufficientCapacityException { + private static final long serialVersionUID = SerialVersionUID.InsufficientCapacityException; + + public InsufficientVirtualNetworkCapcityException(String msg) { + super(msg); + } +} diff --git a/api/src/com/cloud/exception/InternalErrorException.java b/api/src/com/cloud/exception/InternalErrorException.java new file mode 100644 index 00000000000..9acc31ded00 --- /dev/null +++ b/api/src/com/cloud/exception/InternalErrorException.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.exception; + +public class InternalErrorException extends ManagementServerException { + + + private static final long serialVersionUID = -3070582946175427902L; + + public InternalErrorException(String message) { + super(message); + } + + + +} diff --git a/api/src/com/cloud/exception/InvalidParameterValueException.java b/api/src/com/cloud/exception/InvalidParameterValueException.java new file mode 100644 index 00000000000..e6b2dcd2a4a --- /dev/null +++ b/api/src/com/cloud/exception/InvalidParameterValueException.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.exception; + +/** + * @author chiradeep + * + */ +public class InvalidParameterValueException extends ManagementServerException { + + private static final long serialVersionUID = -2232066904895010203L; + + public InvalidParameterValueException(String message) { + super(message); + + } + +} diff --git a/api/src/com/cloud/exception/ManagementServerException.java b/api/src/com/cloud/exception/ManagementServerException.java new file mode 100644 index 00000000000..41ba73d138a --- /dev/null +++ b/api/src/com/cloud/exception/ManagementServerException.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +/** + * @author chiradeep + * + */ +public class ManagementServerException extends Exception { + + + private static final long serialVersionUID = SerialVersionUID.ManagementServerException; + + + public ManagementServerException() { + + } + + + public ManagementServerException(String message) { + super(message); + } + + + + +} diff --git a/api/src/com/cloud/exception/NetworkRuleConflictException.java b/api/src/com/cloud/exception/NetworkRuleConflictException.java new file mode 100644 index 00000000000..fe8781b4487 --- /dev/null +++ b/api/src/com/cloud/exception/NetworkRuleConflictException.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.exception; + +public class NetworkRuleConflictException extends + ManagementServerException { + + private static final long serialVersionUID = -294905017911859479L; + + public NetworkRuleConflictException(String message) { + super(message); + } + +} diff --git a/api/src/com/cloud/exception/PermissionDeniedException.java b/api/src/com/cloud/exception/PermissionDeniedException.java new file mode 100644 index 00000000000..25f02f57fc0 --- /dev/null +++ b/api/src/com/cloud/exception/PermissionDeniedException.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +/** + * @author chiradeep + * + */ +public class PermissionDeniedException extends ManagementServerException { + + private static final long serialVersionUID = -4631412831814398074L; + + public PermissionDeniedException(String message) { + super(message); + // TODO Auto-generated constructor stub + } + +} diff --git a/api/src/com/cloud/exception/ResourceAllocationException.java b/api/src/com/cloud/exception/ResourceAllocationException.java new file mode 100644 index 00000000000..be0c2bb7c72 --- /dev/null +++ b/api/src/com/cloud/exception/ResourceAllocationException.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.exception; + +public class ResourceAllocationException extends ManagementServerException { + + private static final long serialVersionUID = -2232066904895010203L; + private String resourceType; + + public ResourceAllocationException(String message) { + super(message); + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getResourceType() { + return this.resourceType; + } + +} diff --git a/api/src/com/cloud/exception/ResourceInUseException.java b/api/src/com/cloud/exception/ResourceInUseException.java new file mode 100644 index 00000000000..d622b4cc924 --- /dev/null +++ b/api/src/com/cloud/exception/ResourceInUseException.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +/** + * @author chiradeep + * + */ +public class ResourceInUseException extends ManagementServerException { + + private static final long serialVersionUID = 1383416910411639324L; + private String resourceType; + private String resourceName; + + public ResourceInUseException(String message) { + super(message); + } + + public ResourceInUseException(String message, String resourceType, + String resourceName) { + super(message); + this.resourceType = resourceType; + this.resourceName = resourceName; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getResourceType() { + return this.resourceType; + } + + public void setResourceName(String resourceName) { + this.resourceName = resourceName; + } + + public String getResourceName() { + return resourceName; + } +} diff --git a/api/src/com/cloud/exception/StorageUnavailableException.java b/api/src/com/cloud/exception/StorageUnavailableException.java new file mode 100644 index 00000000000..d6ed0265d88 --- /dev/null +++ b/api/src/com/cloud/exception/StorageUnavailableException.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +/** + * This exception is thrown when the storage device can not be reached. + * + */ +public class StorageUnavailableException extends AgentUnavailableException { + + private static final long serialVersionUID = SerialVersionUID.StorageUnavailableException; + + public StorageUnavailableException(long hostId) { + super(hostId); + } + + public StorageUnavailableException(String msg) { + super(msg, -1); + } +} diff --git a/api/src/com/cloud/hypervisor/Hypervisor.java b/api/src/com/cloud/hypervisor/Hypervisor.java new file mode 100644 index 00000000000..17178bd2154 --- /dev/null +++ b/api/src/com/cloud/hypervisor/Hypervisor.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.hypervisor; + +public class Hypervisor { + + public static enum Type { + None, //for storage hosts + Xen, + XenServer, + KVM; + } + +} diff --git a/api/src/com/cloud/network/Network.java b/api/src/com/cloud/network/Network.java new file mode 100644 index 00000000000..c5c35b0d13b --- /dev/null +++ b/api/src/com/cloud/network/Network.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.network; + +/** + * Network includes all of the enums used within networking. + * + */ +public class Network { + /** + * Different ways to assign ip address to this network. + */ + public enum Mode { + None, + Static, + Dhcp, + ExternalDhcp; + }; + + public enum AddressFormat { + Ip4, + Ip6 + } + + /** + * Different types of broadcast domains. + */ + public enum BroadcastDomainType { + Native, + Vlan, + Vswitch, + Vnet; + }; + + /** + * Different types of network traffic in the data center. + */ + public enum TrafficType { + Public, + Guest, + Storage, + LinkLocal, + Vpn, + Management + }; +} diff --git a/api/src/com/cloud/offering/DiskOffering.java b/api/src/com/cloud/offering/DiskOffering.java new file mode 100644 index 00000000000..69e74415790 --- /dev/null +++ b/api/src/com/cloud/offering/DiskOffering.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.offering; + + +/** + * Represents a disk offering that specifies what the end user needs in + * the disk offering. + * + */ +public interface DiskOffering { + long getId(); + + String getUniqueName(); + + boolean getUseLocalStorage(); + + Long getDomainId(); + + String getName(); + + String getDisplayText(); + + long getDiskSizeInBytes(); + + public String getTags(); + + public String[] getTagsArray(); +} diff --git a/api/src/com/cloud/offering/NetworkOffering.java b/api/src/com/cloud/offering/NetworkOffering.java new file mode 100644 index 00000000000..831a626c0f8 --- /dev/null +++ b/api/src/com/cloud/offering/NetworkOffering.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.offering; + + +/** + * Describes network offering + * + */ +public interface NetworkOffering { + public enum GuestIpType { + Virtualized, + DirectSingle, + DirectDual + } + + long getId(); + + /** + * @return name for the network offering. + */ + String getName(); + + /** + * @return text to display to the end user. + */ + String getDisplayText(); + + /** + * @return the rate in megabits per sec to which a VM's network interface is throttled to + */ + Integer getRateMbps(); + + /** + * @return the rate megabits per sec to which a VM's multicast&broadcast traffic is throttled to + */ + Integer getMulticastRateMbps(); + + /** + * @return the type of IP address to allocate as the primary ip address to a guest + */ + GuestIpType getGuestIpType(); + + /** + * @return concurrent connections to be supported. + */ + Integer getConcurrentConnections(); +} diff --git a/api/src/com/cloud/offering/OfferingManager.java b/api/src/com/cloud/offering/OfferingManager.java new file mode 100644 index 00000000000..3d61af05490 --- /dev/null +++ b/api/src/com/cloud/offering/OfferingManager.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.offering; + +/** + * + * OfferingManager manages the different type of service offerings + * available to the administrators of the system. + * + */ +public interface OfferingManager { + /** + * Creates a service offering. + * @return ServiceOffering + */ + ServiceOffering createServiceOffering(); + + /** + * Creates a disk offering. + * @return DiskOffering + */ + DiskOffering createDiskOffering(); + + /** + * Creates a network offering. + * @return NetworkOffering + */ + NetworkOffering createNetworkOffering(); + + +} diff --git a/api/src/com/cloud/offering/ServiceOffering.java b/api/src/com/cloud/offering/ServiceOffering.java new file mode 100755 index 00000000000..4cdaaeb4dce --- /dev/null +++ b/api/src/com/cloud/offering/ServiceOffering.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.offering; + +/** + * ServiceOffering models the different types of service contracts to be + * offered. + */ +public interface ServiceOffering { + public enum GuestIpType { + Virtualized, + DirectSingle, + DirectDual + } + + /** + * @return user readable description + */ + String getName(); + + /** + * @return # of cpu. + */ + int getCpu(); + + /** + * @return speed in mhz + */ + int getSpeed(); + + /** + * @return ram size in megabytes + */ + int getRamSize(); + + /** + * @return Does this service plan offer HA? + */ + boolean getOfferHA(); + + /** + * @return the rate in megabits per sec to which a VM's network interface is throttled to + */ + int getRateMbps(); + + /** + * @return the rate megabits per sec to which a VM's multicast&broadcast traffic is throttled to + */ + int getMulticastRateMbps(); + + /** + * @return the type of IP address to allocate as the primary ip address to a guest + */ + GuestIpType getGuestIpType(); + + /** + * @return whether or not the service offering requires local storage + */ + boolean getUseLocalStorage(); + +} diff --git a/api/src/com/cloud/storage/Storage.java b/api/src/com/cloud/storage/Storage.java new file mode 100644 index 00000000000..08eccfeb8ff --- /dev/null +++ b/api/src/com/cloud/storage/Storage.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +public class Storage { + public enum ImageFormat { + QCOW2(true, true, false), + RAW(false, false, false), + VHD(true, true, true), + ISO(false, false, false); + + private final boolean thinProvisioned; + private final boolean supportSparse; + private final boolean supportSnapshot; + + private ImageFormat(boolean thinProvisioned, boolean supportSparse, boolean supportSnapshot) { + this.thinProvisioned = thinProvisioned; + this.supportSparse = supportSparse; + this.supportSnapshot = supportSnapshot; + } + + public boolean isThinProvisioned() { + return thinProvisioned; + } + + public boolean supportsSparse() { + return supportSparse; + } + + public boolean supportSnapshot() { + return supportSnapshot; + } + + public String getFileExtension() { + return toString().toLowerCase(); + } + } + + public enum FileSystem { + Unknown, + ext3, + ntfs, + fat, + fat32, + ext2, + ext4, + cdfs, + hpfs, + ufs, + hfs, + hfsp + } + + public enum StoragePoolType { + Filesystem(false), //local directory + NetworkFilesystem(true), //NFS or CIFS + IscsiLUN(true), //shared LUN, with a clusterfs overlay + Iscsi(true), //for e.g., ZFS Comstar + ISO(false), // for iso image + LVM(false); // XenServer local LVM SR + + boolean shared; + + StoragePoolType(boolean shared) { + this.shared = shared; + } + + public boolean isShared() { + return shared; + } + } +} diff --git a/api/src/com/cloud/uservm/UserVm.java b/api/src/com/cloud/uservm/UserVm.java new file mode 100755 index 00000000000..c4a9d2c923e --- /dev/null +++ b/api/src/com/cloud/uservm/UserVm.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General License for more details. + * + * You should have received a copy of the GNU General License + * along with this program. If not, see . + * + */ +package com.cloud.uservm; + +import com.cloud.vm.VirtualMachine; + +/** + * This represents one running virtual machine instance. + */ +public interface UserVm extends VirtualMachine { + + /** + * @return service offering id + */ + long getServiceOfferingId(); + + /** + * @return the domain router associated with this vm. + */ + Long getDomainRouterId(); + + /** + * @return the vnet associated with this vm. + */ + String getVnet(); + + /** + * @return the account this vm instance belongs to. + */ + long getAccountId(); + + /** + * @return the domain this vm instance belongs to. + */ + long getDomainId(); + + /** + * @return ip address within the guest network. + */ + String getGuestIpAddress(); + + /** + * @return mac address of the guest network. + */ + String getGuestMacAddress(); + + Long getIsoId(); + + String getDisplayName(); + + String getGroup(); + + String getUserData(); + + void setUserData(String userData); +} diff --git a/api/src/com/cloud/vm/Nic.java b/api/src/com/cloud/vm/Nic.java new file mode 100644 index 00000000000..b477170ed11 --- /dev/null +++ b/api/src/com/cloud/vm/Nic.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + + +/** + * Nic represents one nic on the VM. + */ +public interface Nic { + enum State { + AcquireIp, + IpAcquired, + } + + State getState(); + + String getIp4Address(); + + String getMacAddress(); + + /** + * @return network profile id that this + */ + long getNetworkProfileId(); + + /** + * @return the unique id to reference this nic. + */ + long getId(); + + /** + * @return the vm instance id that this nic belongs to. + */ + long getInstanceId(); +} diff --git a/api/src/com/cloud/vm/State.java b/api/src/com/cloud/vm/State.java new file mode 100644 index 00000000000..81692497d9f --- /dev/null +++ b/api/src/com/cloud/vm/State.java @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.vm; + +import java.util.List; + +import com.cloud.utils.fsm.StateMachine; + +public enum State { + Creating(true), + Starting(true), + Running(false), + Stopping(true), + Stopped(false), + Destroyed(false), + Expunging(true), + Migrating(true), + Error(false), + Unknown(false); + + private final boolean _transitional; + + private State(boolean transitional) { + _transitional = transitional; + } + + public boolean isTransitional() { + return _transitional; + } + + public static String[] toStrings(State... states) { + String[] strs = new String[states.length]; + for (int i = 0; i < states.length; i++) { + strs[i] = states[i].toString(); + } + + return strs; + } + + public State getNextState(VirtualMachine.Event e) { + return s_fsm.getNextState(this, e); + } + + public State[] getFromStates(VirtualMachine.Event e) { + List from = s_fsm.getFromStates(this, e); + return from.toArray(new State[from.size()]); + } + + protected static final StateMachine s_fsm = new StateMachine(); + static { + s_fsm.addTransition(null, VirtualMachine.Event.CreateRequested, State.Creating); + s_fsm.addTransition(State.Creating, VirtualMachine.Event.OperationSucceeded, State.Stopped); + s_fsm.addTransition(State.Creating, VirtualMachine.Event.OperationFailed, State.Destroyed); + s_fsm.addTransition(State.Stopped, VirtualMachine.Event.StartRequested, State.Starting); + s_fsm.addTransition(State.Stopped, VirtualMachine.Event.DestroyRequested, State.Destroyed); + s_fsm.addTransition(State.Stopped, VirtualMachine.Event.StopRequested, State.Stopped); + s_fsm.addTransition(State.Stopped, VirtualMachine.Event.AgentReportStopped, State.Stopped); + s_fsm.addTransition(State.Starting, VirtualMachine.Event.OperationRetry, State.Starting); + s_fsm.addTransition(State.Starting, VirtualMachine.Event.OperationSucceeded, State.Running); + s_fsm.addTransition(State.Starting, VirtualMachine.Event.OperationFailed, State.Stopped); + s_fsm.addTransition(State.Starting, VirtualMachine.Event.AgentReportRunning, State.Running); + s_fsm.addTransition(State.Starting, VirtualMachine.Event.AgentReportStopped, State.Stopped); + s_fsm.addTransition(State.Destroyed, VirtualMachine.Event.RecoveryRequested, State.Stopped); + s_fsm.addTransition(State.Destroyed, VirtualMachine.Event.ExpungeOperation, State.Expunging); + s_fsm.addTransition(State.Creating, VirtualMachine.Event.MigrationRequested, State.Destroyed); + s_fsm.addTransition(State.Running, VirtualMachine.Event.MigrationRequested, State.Migrating); + s_fsm.addTransition(State.Running, VirtualMachine.Event.AgentReportRunning, State.Running); + s_fsm.addTransition(State.Running, VirtualMachine.Event.AgentReportStopped, State.Stopped); + s_fsm.addTransition(State.Running, VirtualMachine.Event.StopRequested, State.Stopping); + s_fsm.addTransition(State.Migrating, VirtualMachine.Event.MigrationRequested, State.Migrating); + s_fsm.addTransition(State.Migrating, VirtualMachine.Event.OperationSucceeded, State.Running); + s_fsm.addTransition(State.Migrating, VirtualMachine.Event.OperationFailed, State.Running); + s_fsm.addTransition(State.Migrating, VirtualMachine.Event.AgentReportRunning, State.Running); + s_fsm.addTransition(State.Migrating, VirtualMachine.Event.AgentReportStopped, State.Stopped); + s_fsm.addTransition(State.Stopping, VirtualMachine.Event.OperationSucceeded, State.Stopped); + s_fsm.addTransition(State.Stopping, VirtualMachine.Event.OperationFailed, State.Running); + s_fsm.addTransition(State.Stopping, VirtualMachine.Event.AgentReportRunning, State.Running); + s_fsm.addTransition(State.Stopping, VirtualMachine.Event.AgentReportStopped, State.Stopped); + s_fsm.addTransition(State.Stopping, VirtualMachine.Event.StopRequested, State.Stopping); + s_fsm.addTransition(State.Expunging, VirtualMachine.Event.OperationFailed, State.Expunging); + s_fsm.addTransition(State.Expunging, VirtualMachine.Event.ExpungeOperation, State.Expunging); + } +} diff --git a/api/src/com/cloud/vm/VirtualMachine.java b/api/src/com/cloud/vm/VirtualMachine.java new file mode 100755 index 00000000000..b8a7646a8d9 --- /dev/null +++ b/api/src/com/cloud/vm/VirtualMachine.java @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +import java.util.Date; + +/** + * VirtualMachine describes the properties held by a virtual machine + * + */ +public interface VirtualMachine { + public enum Event { + CreateRequested, + StartRequested, + StopRequested, + DestroyRequested, + RecoveryRequested, + AgentReportStopped, + AgentReportRunning, + MigrationRequested, + ExpungeOperation, + OperationSucceeded, + OperationFailed, + OperationRetry, + OperationCancelled + }; + + public enum Type { + User, + DomainRouter, + ConsoleProxy, + SecondaryStorageVm + } + + public String getInstanceName(); + + /** + * @return the id of this virtual machine. null means the id has not been set. + */ + public long getId(); + + /** + * @return the name of the virtual machine. + */ + public String getName(); + + /** + * @return the ip address of the virtual machine. + */ + public String getPrivateIpAddress(); + + /** + * @return mac address. + */ + public String getPrivateMacAddress(); + + /** + * @return password of the host for vnc purposes. + */ + public String getVncPassword(); + + /** + * @return the state of the virtual machine + */ + public State getState(); + + /** + * @return template id. + */ + public long getTemplateId(); + + /** + * returns the guest OS ID + * @return guestOSId + */ + public long getGuestOSId(); + + /** + * @return pod id. + */ + public long getPodId(); + + /** + * @return data center id. + */ + public long getDataCenterId(); + + /** + * @return id of the host it is running on. If not running, returns null. + */ + public Long getHostId(); + + /** + * @return id of the host it was assigned last time. + */ + public Long getLastHostId(); + + /** + * @return should HA be enabled for this machine? + */ + public boolean isHaEnabled(); + + /** + * @return date when machine was created + */ + public Date getCreated(); + + Type getType(); +} diff --git a/api/src/com/cloud/vm/VmCharacteristics.java b/api/src/com/cloud/vm/VmCharacteristics.java new file mode 100644 index 00000000000..6df26121c30 --- /dev/null +++ b/api/src/com/cloud/vm/VmCharacteristics.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +import java.util.Map; + +import com.cloud.hypervisor.Hypervisor; + +public class VmCharacteristics { + int core; + int speed; // in mhz + long ram; // in bytes + Hypervisor.Type hypervisorType; + VirtualMachine.Type type; + + Map params; + + public VmCharacteristics(VirtualMachine.Type type) { + this.type = type; + } + + public VirtualMachine.Type getType() { + return type; + } + + + public VmCharacteristics() { + } + + public int getCores() { + return core; + } + + public int getSpeed() { + return speed; + } + + public long getRam() { + return ram; + } + + public Hypervisor.Type getHypervisorType() { + return hypervisorType; + } + + public VmCharacteristics(int core, int speed, long ram, Hypervisor.Type type, Map params) { + this.core = core; + this.speed = speed; + this.ram = ram; + this.hypervisorType = type; + this.params = params; + } +} \ No newline at end of file diff --git a/build.xml b/build.xml new file mode 100755 index 00000000000..f03c94b4734 --- /dev/null +++ b/build.xml @@ -0,0 +1,78 @@ + + + + + + This is the overall dispatch file. It includes other build + files but doesnot provide targets of its own. Do not modify + this file. If you need to create your own targets, modify the + developer.xml. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/build-cloud.properties b/build/build-cloud.properties new file mode 100755 index 00000000000..4ee5aba22c3 --- /dev/null +++ b/build/build-cloud.properties @@ -0,0 +1,40 @@ +debug=true +debuglevel=lines,source,vars +debug.jvmarg=-Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n +deprecation=off +build.type=developer +target.compat.version=1.6 +source.compat.version=1.6 +branding.name=default + +# +# Set your own default instance for server-server-dev.xml +# + +#default.zone=KY +#default.instance=KY + +# +# Set your own log directory +# for production build set logdir=/var/log/vmops +# +#logdir=logs + + +# +# Set your own KVM developer.properties values +# + +#private.macaddr.start=00:16:3e:77:03:01 +#private.ipaddr.start=192.168.168.128 + + +# +# Set your own agent.properties values +# + +#WORKERS=3 +#HOST=192.168.1.190 +#PORT=8250 +#POD=KY +#ZONE=KY diff --git a/build/build-cloud.xml b/build/build-cloud.xml new file mode 100755 index 00000000000..fde4bb491f6 --- /dev/null +++ b/build/build-cloud.xml @@ -0,0 +1,566 @@ + + + + + + Cloud Stack ant build file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/build-common.xml b/build/build-common.xml new file mode 100755 index 00000000000..ae2f814f1bd --- /dev/null +++ b/build/build-common.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/build-docs.xml b/build/build-docs.xml new file mode 100755 index 00000000000..c0695f04182 --- /dev/null +++ b/build/build-docs.xml @@ -0,0 +1,66 @@ + + + + + + Cloud Stack ant build file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/build.number b/build/build.number new file mode 100644 index 00000000000..96105fe8c54 --- /dev/null +++ b/build/build.number @@ -0,0 +1,3 @@ +#Build Number for ANT. Do not edit! +#Sat Aug 07 12:54:57 PDT 2010 +build.number=927 diff --git a/build/cloud.properties b/build/cloud.properties new file mode 100755 index 00000000000..62cf76b9da1 --- /dev/null +++ b/build/cloud.properties @@ -0,0 +1,14 @@ +# Copyright 2007 VMOps, Inc. + +# major.minor.patch versioning scheme for vmops +company.major.version=1 +company.minor.version=9 +company.patch.version=1 + +svn.revision=2 + +# copyright year +company.copyright.year=2008-2010 +company.url=http://www.vmops.com +company.license.name=GPL +company.name=VMOps Inc. diff --git a/build/deploy/branding/default/images/favicon.ico b/build/deploy/branding/default/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..97a1e14bd3c4f1e0e4d48011ce1d89d7e1fab606 GIT binary patch literal 1406 zcmZQzU<5(|0R}M0U}azs1F|%L7$l?s#Ec9aKoZP=&`9jvWtAj8AMlNp|#Uc~VG+XsXhqpZ;o7!85p7XnO- zoWN3pk&S_q5#%{`Mif39BMP6H2bIss$iv0R!obYP$N|z1)XV|ItgMWTEL=b{85y~O RVoaQjj9fq=pglty007J=A3Xp7 literal 0 HcmV?d00001 diff --git a/build/deploy/branding/default/images/header_logo.gif b/build/deploy/branding/default/images/header_logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..a1eb06dbf8fce4e942c12185cf9740e673a1e38e GIT binary patch literal 5319 zcmWkxdt6M37r*z;otZnY=1xTqO{PLha;2BZPS5oyrm#hpVcUdVZcicYZJKHnYbveQ zBey)(5+*|4w+BV3DLoX+kVlf--lfO<{Lc4$&R^%B@A-bu=X}nO%lzFJZmtHLfMEd0 z%{|i6H5OXXH1_S=lJvW0o_x?eYCc>t*x%Uq3-o(d46r*g#KgK_vwuc4Gpa)>&NxuqL{np z@&D$Qy?oQ&5^&_f~1(a`~V#5nq< zaooMnU7a63e6&ffO1b&MCh4B+XiaW0Wrbgyf?w_Ed_O)u?2z{OySxJfLoF?>=cWgk9`To zQTK$gRg;sGUQEcQxoRCD~K*Lih4Y@s*^D)uSV{qNILIKR^(K_>Tu6mpeXx9WK)i zeD3P~^rfqt?#a*3KV5K&neecxs(NL9GMZ;exjXo;o%=#4oybB zuDf?<_^YlWQ?GyaZ`Lt8C+nhxPSmJ$SbHkdDz*~7>A$tkxxN0u1HB$#&DIekfelM- zD@v0YqVrgKW(@uCqd6-x%cy+6Yxwy$X%_LFZ_*`cc~)*)D`gLjSvuf}g%*Jel-lOx z<*r**Mk$sN2Q?IA(^(2Ep3%8E#objYOPy|=N1n-+%|N~RR(E?T26N45rSS}WwI7mX zu?@;yJ5}z@cTGdfjl;Y@u{lm>=oRxA1W@kM;M)NrmemakuInBRh_r;ulrECr=&hpI zmY~@Pf(HBYEMF?6V#QnQrzVK*Eh#lvG8mx5Hs-95ghy{H7g+{;@V(Fwdo2Q)q0)Dx zyW7aM3TJyltMEwD76Yg2{U}WB0nN|#p-AY&ajMJ6ZEh+BEtvbEO}?+ zWfp3j3_wAh$Gn^oGy_1)0?@fO4FQBy6Y95GKuYAPQc;`?&zWncgAN(3`c?BuI(NIh ztQ_Oc*e^}<$hp4gIEWlPlljrMr5a?h zEJKY1R?*@3*L!AkRx)|Zyy#jc!}#5Xa&k$?A=_m1XCKH5(r*r&kzwxwTI^D#2+b1@ zOMVm`bh`tHqE$h@&c^fsFK06eRRw0B84EC;71HnLqQ1Fx-}r|;|ClL6KrHvq`dsAS9uv3GvZQyKU=l^|TZBB~CeLyjLu@5g2 z+c3PHOktI-g!q)g!t@O|OHP{O)yB8+US_Zg1dS=cCIb`1b9OLR-#TWqo=O)GR_39q zM}iDu3ieKD5>?@69;~xY<-k(}MoWkEp#=<&ge%}{pY&PzZ;;>;z$9d!lwqnm3_V|F zeh`7UVH$vm5C-#r)Y22OMZwQ`V0LNcv{yh1fIVRh8u>VrI9DEf1&qZ>$Mv8A7h@Vo zOMQ$8sa>q+hXl(6!uWm^?ABdwa6zqnm+wV+{AyreZK`BR3Kj(PE;E0HOHEedQ2Ys! zw=8m1W_~%wY4b)NP;7CMmRQf#v8S$>f?P=Bq0%>k>l+})*_t2xtCOKc2`L6iQnp8) zlvy7ca`K%-;N}dN#-dV(YLY2j{L9hVi3C720P8;lP2C5xr=EzPVmZSbZX`3=XM)nL zRbI^b=V9@BjXq_HsTX5h(wZ-of#wy5+vIQ?axfO1a-9I;%9=5uNxT4tA|QT7F;m(pDe!91Lrf=PURh>iZW;<%J{a*6S8p-9BqwIYx`5kCE6g%3%#u78mN_MTlqN72AwV~oM;{ABW4La_7>oA)5+ z{bm2*87$Rl?snX7Q6iSM-dDpkQa!SKDTmi?02uRpDOPA2JG|tV=tb^IvBP{gVR2mW z?Mpt+<2W)6CrnpoJ~B~I274v%>;}#Q_GGW|2Y1_@#}&eu^`Cqc&Og~?5e_qtsgdQf zEhZ^>zd60a84KeDa47X~K1}iaJ!df(IzQKa-_jy}a*ydYrd#M8Dd?wc}vDrC1)4`EAubDb=Q*W~}&nx^m1|u^dv+DCo z;~Q$#X4TWFB0s?NKA8?-_HsYiu>$Tc}dhOMxz`!?0ZD7xmp`e3|{BFWM;Ot+{Pfj z6M&t`@_@zR>q%xB>9^)n(>OH>Nt%R;iFg zZeH=B!c~G2AsUjkDKg$-C7`mFgg?0L?ls5q9kI7$%k+WLDyM33ghB=aQ zt*L1NN34~yZ^+rq(jo&(v=vN~!;6e9p#>ZL=N1v%E04FyZdOBvsN~AKmB~hd@d{@H zt#a9UIVV_>Y!S=5#>B17nmeoPmtro$k~i=z)sR;nbHHS>#R-Z~p@ z4vz3>TR9G-Q&58h{(>_LsRd>dD4hlxXvjgDxa!ya4Jec2wW$<`W@@bsWT3Nb(_kuZ zK+7ER6ZTx+dBTu2rnT0peYWowY3ZG5AzS0p_~zu+BXYPaorP15TeML2EJri#4+xPW zrXX`Qae#(4;Vx5$Aak6FJpxe;(O(M!;j|JHZkz&MC*PAl6INk}0u^xO>=AWrgATAI7H*t{+UfxJGa1iqlb+#;MU+Kw zDJ!$tdJV>clj9{>sePJM2Q?C?;~&P5bO21zf?^ED0O(KJ%S^slY#J0Tj}_xc`&lq1 z(l)vrE|n8_h+M3Nxx~T~L;My%n5#~z2Een}F9gchNu>_I*yTM#8~~=PEqkjLUPXIP zqgEZDAX98#AkJ&J3f?975Y4wFfj~0FQU+Z-pU5n;sI^koCTD#*7g6iQwABWA#6nZ9 z^Ka*2Q4D43pvY3P^&*;1+HQB?U2CQ6sxs z8HcmM7AZFl5Eh_^<26t^1}~v=H=9{WBwVo^1_3rx!%M<;)MNX62!2otBTO5fjlmi{ z4(}}lf=PC_0ystShw(gKM-VSYXe(t36G3LAPM#yMZ_55NdR6_ zlaE&!PqloMMuOERHnwq}u0vN>2V%;e%5XDogK#jeOHMQt+LKA0Tix0AtgI7J)nPw~3k z1!lCJ1n0-&NC?XH!I{(Bp*S)Op^z#J8F>Wx%edwwP`SmYBO0*K@B+1P!S`dQLB3JU z{N?Btl9_`cGCB7p9=VB@NYUuk$v>>1k;fWnzf6#0CGbSKGo%F!!qHhHTzz!YGeB?& zNP3&byMO=-==@SW#@@7jQ~h73pcL-RZNIbHMV5r%y855Ao0kf=!56jgVHroIWoJ{f zYm7J^F`V8^;?3>Knch|%baDEA@-ZTweL;ek3b zAc*43kqd=VsaUm;rjan&BD#>1BH@S8u%9$wYX z8Eb&Nbu2ox8;gs|fo-)ceu^M?Huxa&Qg-i7l`aWM_{Y2zWVcT8TIa`-TQ;-VaOqF9C+4J5KN)5`$iKDIY1 zn2W+h0&q>rQ!WBG5%3X=b24V(G%X{CV09GE27Ta`5O7cifwzTMRNT4Cl-&e(0Ee7) zfL$0^L*~V(DL@T_8$jKSvnLJ+`gL6A10`;^m?N%m_HJev%DCtQ6%#BES~v$ic~Omo zuK+5g5QlQ3wCo{l*8wTdT*Ev`uqvf|mxHY7+UNxyFlz_!4MkpM@>Xo-xskvs84!(u zb9G2a4p>OTpQ`77bb{v5K)rg-X9-(M0ukT8L0r(HO94{^zoX=U4Ei0o&CJNE&4QN6 zgv$u1|14%;4X&-KiVN8P3(L@63X^&{wEZD?0RJaYdMA7F<0Idb#8}`O&R_|98)u z+YUhvzJS49Ij5!+s(o2oH(sl?c-px5Y13UOP{Ld+VH{Vpt`n-0hPBcco1#T-w*Z4= z9O$G$SX0;ivaV;mj<%@pTU`G+fHjlg>_Pdz;;`egTF1@%h1tr>y-t2I}g9!rFRx@c6i zS_6h7Xw_9bf@lRf#8QtH#2*xG1jQ{X8Wl92@UQ0WqyjJ# zqyymkk<9{N6ifrm_!cW19UWyPO&eWZ;VcDas5csDiYAbHy+p59khGD`hhz#2MPwSi zPN~yiiV_Sh!3Y7OkSP%vh8bau!~{lIKw_i@BL&P1Ga4`>1}hA(kpYI02KXB=3@O2k z8q6qw5hkvI89{1bQlP{r5k@Ellt@6xAQ{ADhGZC_!3Yea3>byMr~#%JU>IS5zX8Lb z5{%MdlmLd9xCUkfp@9hjEr*a`R7XH2qe-bCx+tnkujtZix@d()FOeazGQWf-P?}Ka zm>y}ki`M7}R71lWTBD&ACK}ceq>fNPut>+GlEd|S2!#YXIYARfTB4vOMnoW|36ho| zw1iY50+KM25(Oz?7$GGjEk{T>Ny-sgt{~+KT5hCa1q~Z%IZ49^4Kqtd5>}8f!w3nJ zN)&?>0-B&`2}(;S=v#7vmO!+GP(n;{ij<(Fgkgx35VRa6UAsQw~ zIYq)K2{R0lFk!0i58?b{xMS2PjRFzEY}1GsL25*#01B2V(Gn$PP*Ru(HHat% z%q<4dH(*MLS<)yefe2#a8WAH1jffDG=ouFngP7Nrn7(PFhNeukMz7Za7%CwM6JtR= zRIhJ^f(2dQ>X+2hOxX$)g)jvLD+mmsV1&R>3PuSGqF{(HFy84~WvDz$!5lQ!)09ks zWT6m>B3a)Aq9+Ih5_OTSG6iHBHJE6MG8zGZP5=M-&?sO*6C!K0P1_(lucd7@FU z%SBL>{3sB7V#nMqOkQxU$R%XQ1j{#-0C+_F2gyvFbcN`$_MDHjg1--ovs9X*JCktp zapB7*K&$ffV}f$LmR&E=Hl$X9bXAsg(v`eQ`*}|mxTBq?6T_A-@Z~;Hna^i^=p4Hs zW${*zmg}V_yif4+Iuo{p@>&N1wd zk|ht^J^(mJ_478SUYoD&O$eDOGR`Xdv2x`y@4Ks>J#hQ@u^+qY<`FZtV!-d>X=nnG zwkLhBQno#k6;ll zG$R?Y)>Q4D6Dv%e-$lZHrHT8Z{qC_rYxZZ)rd_m* zcB(||SaS8S#Y{)=LpP^gd&qoB2A37}K~^raSo-~K&M=-~qlv3oqz3o1Gq@oQRv__| zxuYlWSth=)8?Of1&^g%h6UP9Xb8kjaH)m&Y;2;MYJaTW)JWTsZoZ|8`eE)8ZI5Ag+ zi&+odYSfhKAu3zBL>*Y%Fb?op*{b~Wz7gn2>E~?Gr&JJ#GR@alVkhfh10cB)|IzG8D8H~GQ7#eQs?9Z9*txB0K& z#^X{p*x7#8(lKAVZT0A2F7nm;)B(N3D+$*RT8nM`&0;QPBlo}Dx4s$XcqVoGr{udH z*Q{D6H%2|Y`eYZPE3%8`3Aj)H*l9K#@>c5EQ+iU_aP(v-wfR*5DMdN$HmAG4`0CzS z&&?MWdmhNVb3|+fM+0*x;aFLB>XR!T+I9hQ}#?hZ*I2gdRr7n zT^y<(a5J@BJ0)GuW!F!gYwq2CWc3t0+!A*Jop(UyQ-qb4xB3bX`pSO%=kyG@@NPK&%?BW`JwG(1V-dN{-kY`p8{3;#@+MSa9cu8cU4Di>tR9J?F znU(*B)T+<9C*0r<+x~QKjXmD{@p$BoOpMw!7H5_2Kxb`pa*?pS<6W8@v4q0z^Sv!; zpj}b$rOzk#Lrdxe@61I#&Hf3)_3T+qPEbOG{i6QUG1+ceAL}03Ki(VgmYeo-kP(=bs8!kfufu;vO6~ZysU`kHrjn&3$qr3H0m`^(=U<6J^;d$`>) z!!y&VC$geq%GfbxAAYfV=03(uQ)9i+p`6v>{V%HPR z+F&!D%qu>*1z6zC<4?uKc%`_p%oEN0Eqblli8D(p63V68MAD)yJ>EZRw*eC3?x8#W z03gYy*R>9)Au!Z-za%&6G{4@{@#_HF0s7byQzutr>cWWMd~vt_7H4x15;5bKdDBPd z^(|s20ZohruDltt?-ATO-g*&;=V+bYeNYwMt+rYD^t!6p52E7oi?@}BZa(a&?m77H z)aBu|e39SwIU}?1iEHlKy~_<=7{ubvU^f;XX}so9mY%=y;S=bUw$S^}l^4%hh9*43 z?ZObL%m`#@FoW{z>|f{^p>w^R3)p+R2M;%Q^yu z;%#-imiIVDW>vLY^xeOcUh1dQIXDl4=*}|UcKC@_*bH8gq z(~C55`NSo0IYFpvwUtkPVi>Br5qWN)fPeBwz6jV=;2>F+bMQ!;kAqe;W@^4_+{1fg zj~xGaB2J1$jNrHRAm8ca=8T5h7ya1v1@9_UX4e)V9C=e1`3JuQ z*$_N&{XSSCSZ~)Ke(v{wvK^i(`*eA=LF!>L<#p|gl(j{Bd6`_d+)$!5dI=@NQ&!{~|;S sa{o3h3wShFvUI;z{F~*2q14!(B2V(@kapzV(XI7^t74UTY&P)wA2sSt2LJ#7 literal 0 HcmV?d00001 diff --git a/build/deploy/branding/nframe/images/header_logo.gif b/build/deploy/branding/nframe/images/header_logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..d1ac0e0e8ca2877148dc92d1cb87836600d07fe3 GIT binary patch literal 2774 zcmW+#c|26>8-HtzEtHhX8Y0quB#dpimP*lCp3n0<-|aca=ubm6b^Axa04xANkPjyaj37{gKnMbm6^;`) zM&KxcBLoh}CWaFjMqns`Ap{1fX&3<11;=5~&@GG- z<^g5GZ5RjC1Pw!nP&EXDqTwc#1$97Thz+3uc_MNewqXq)fER;7LVu7JRtO+J4l>vE zdy~!e6Ou8nUxl1q51LGUJv(xBJr42+>kdhzb(KCXfLDUyK~IoM&pnmy zmI|~3!7*Q;;tLLVuCA^Dc^9DQ4peS|qdwq>H&F8fsto##B%l=tGy?$b_PbACH|PXW z^+E)V!?u}6NLfekyBMc_J;|JrVIQ976<-qcsEV0g_qh0DVQt6zFT?Ev<3r=K^9xJM zE2~p;zgO4R_Csz!{tJMY02_EEjPoB?hw_Sil8euOQWL&K&Lr8mAk83Tr0POXLqU2y zQ~ZTD<3m_xUuI8tHeJ;>9Vz|-qJnDxvV}ILoam2jfAy>u$ z`f%XHwf?Gr+PTheO&)~5>nRP7c5t3@a5g}GYL@b&E$L*CcI9oZU!6&Ahc4-aj#z$q z=uNS|%HuzkGB3p#36^UL-4aQO(c0MSC`1#^(C62tQX?Mu1<54Z*?+CxyG6BocZRIP zObcSJHkRXSt~Si#2@bEjup==-&d)}cp~%j1P+#mon$-2Ojm>FG0%4Bcx2HPf?hS6h z(paN8RwrrS_mmE*{Zv!$4d>#&yqjs|Q#7DUS5OjDzsOEBr!Di&4+mSa6Ec|Ag^Vk) zN9sxkRVDh=&={%-!_t*^s)lK_wiw4s7nu!3=}wOpGan~MS}{|?^%%)%e7SEfNTTib zJlS0Tg`St(4~YDa&+0u!C3P&a$~x0G#y_4wy%kPx^zqiMR;FVco6+P*nO#@a?8W?~ z($LsQRtTstY0kmumU?#7)f+t9dbn-s)Av`5^wPQ4tc~(1^5xF4-T6a8>LChz2eMit zv(q05?lRs_ALQpgSbb;vNQ6Wq5Dds#Nj?tdmxEZoWUM;Ma7H*dljTr+hA+N8QaVegI z@p1cUqk+9_&8Y-8wqM%{muODEp3q6wY;Sn)kB;WFTc>&Y9d)FnVF@$PB~D}^J|p~3rs|84nL8yjs7O}*29c%<)7y#gIQG3;jo)E zsrW;m*J@iuh=Yg*iP<&gz(u#&8>PC-ugusg`=}*JAdar?%#D=X+Y1ID(NSX9IO8a%MNrJg5qm$$ePH?8 z2c0zG1Xq;)ZwZ`jaLj^H6x4Fk|{dV;~ zn{P!tcdS@qRQm${y0VYgLM6H*vQKB4v}K>O&emDJ{)fSsp@>OzLXY!zL+YuPSoIvY z-jeKdU&`7m{y6nd$DZLmJGk4^Z+PT*nC@bJ7T%n)^eZn&{%*A4uFvQBhtuwGMoog0 zY*fmWQFRYREwft54#}{>ZVz6zmo?81Jd*ZuJmzq*l@2Z%l~!6Nw)}Tmyw=laFWFPV z&W=gDGl@xk$#J`6je)zieS0pdm_jHcE$5M z&9pu#+s{_jNO!VkXuto7cD5^dy|}wTbtEeB`_dmuzk@wua{ZYB6_g`VF-+B^R=Z`P z%hlf*JnF4ZOa{LAW{USHIGijcxanO(p_pfcwDaauEZO?6+b8ea-naKVb6os{0`=ib zkACrRfpYM(A-ry!9y#4dk^V311wL=v$Jl*#w{iO!njwzvb;3X9JSp@&-eVusq3^hu zKOI_Aj+$f^OI=1^__R6qi#qOl?e;(SyO_Y$sf!B@&QiU;d~;X%`fUt;&KRY<^yr&! z_pJ=EvefPow^Q}+`m%bZAnIjx?=K$>@wKc1i=%f~y{g%bJe+z@!9k`^m{R4!nQ0g1 tg!9?6s!G?iBXlw@Y#HFcA~oH>(H;*;^$rr3Z+@CvF>2-($HfI`{|9$iSY!YI literal 0 HcmV?d00001 diff --git a/build/deploy/branding/superb/images/header_logo.gif b/build/deploy/branding/superb/images/header_logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..4dee91633aca26b49435e056e68dae725f45567a GIT binary patch literal 2837 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 + exit 1; +fi + +if [ ! -f $1 ]; then + echo "Error: Unable to find $1" + exit 2 +fi + +if [ "$2" != "" ]; then + if [ ! -f $2 ]; then + echo "Error: Unable to find $2" + exit 3 + fi +fi + +if [ ! -f create-database.sql ]; then + printf "Error: Unable to find create-database.sql\n" + exit 4 +fi + +if [ ! -f create-schema.sql ]; then + printf "Error: Unable to find create-schema.sql\n" + exit 5 +fi + +if [ ! -f create-index-fk.sql ]; then + printf "Error: Unable to find create-index-fk.sql\n" + exit 6; +fi + +PATHSEP=':' +if [[ $OSTYPE == "cygwin" ]] ; then + export CATALINA_HOME=`cygpath -m $CATALINA_HOME` + PATHSEP=';' +else + mysql="mysql" + service mysql status > /dev/null 2>/dev/null + if [ $? -eq 1 ]; then + mysql="mysqld" + service mysqld status > /dev/null 2>/dev/null + if [ $? -ne 0 ]; then + printf "Unable to find mysql daemon\n" + exit 7 + fi + fi + + echo "Starting mysql" + service $mysql start > /dev/null 2>/dev/null + +fi + +echo "Recreating Database." +mysql --user=root --password=$3 < create-database.sql > /dev/null 2>/dev/null +mysqlout=$? +if [ $mysqlout -eq 1 ]; then + printf "Please enter root password for MySQL.\n" + mysql --user=root --password < create-database.sql + if [ $? -ne 0 ]; then + printf "Error: Cannot execute create-database.sql\n" + exit 10 + fi +elif [ $mysqlout -ne 0 ]; then + printf "Error: Cannot execute create-database.sql\n" + exit 11 +fi + +mysql --user=cloud --password=cloud cloud < create-schema.sql +if [ $? -ne 0 ]; then + printf "Error: Cannot execute create-schema.sql\n" + exit 11 +fi + +if [ "$1" != "" ]; then + mysql --user=cloud --password=cloud cloud < $1 + if [ $? -ne 0 ]; then + printf "Error: Cannot execute $1\n" + exit 12 + fi +fi + +if [ "$2" != "" ]; then + echo "Adding Templates" + mysql --user=cloud --password=cloud cloud < $2 + if [ $? -ne 0 ]; then + printf "Error: Cannot execute $2\n" + exit 12 + fi +fi + + +echo "Creating Indice and Foreign Keys" +mysql --user=cloud --password=cloud cloud < create-index-fk.sql +if [ $? -ne 0 ]; then + printf "Error: Cannot execute create-index-fk.sql\n" + exit 13 +fi diff --git a/build/deploy/db/log4j.properties b/build/deploy/db/log4j.properties new file mode 100644 index 00000000000..2cf1f24d2fc --- /dev/null +++ b/build/deploy/db/log4j.properties @@ -0,0 +1,7 @@ +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n +log4j.appender.stdout.threshold=ERROR +log4j.rootLogger=INFO, stdout +log4j.category.org.apache=INFO, stdout diff --git a/build/deploy/deploy-agent.sh b/build/deploy/deploy-agent.sh new file mode 100755 index 00000000000..4faa4cb8516 --- /dev/null +++ b/build/deploy/deploy-agent.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +# install.sh -- installs an agent +# +# + +usage() { + printf "Usage: %s: -d [directory to deploy to] -t [routing|storage|computing] -z [zip file] -h [host] -p [pod] -c [data center] -m [expert|novice|setup]\n" $(basename $0) >&2 +} + +mode= +host= +pod= +zone= + +deploydir= +confdir= +zipfile= +typ= + +#set -x + +while getopts 'd:z:t:x:m:h:p:c:' OPTION +do + case "$OPTION" in + d) deploydir="$OPTARG" + ;; + z) zipfile="$OPTARG" + ;; + t) typ="$OPTARG" + ;; + m) mode="$OPTARG" + ;; + h) host="$OPTARG" + ;; + p) pod="$OPTARG" + ;; + c) zone="$OPTARG" + ;; + ?) usage + exit 2 + ;; + esac +done + +printf "NOTE: You must have root privileges to install and run this program.\n" + +if [ "$typ" == "" ]; then + if [ "$mode" != "expert" ] + then + printf "Type of agent to install [routing|computing|storage]: " + read typ + fi +fi +if [ "$typ" != "computing" ] && [ "$typ" != "routing" ] && [ "$typ" != "storage" ] +then + printf "ERROR: The choices are computing, routing, or storage.\n" + exit 4 +fi + +if [ "$host" == "" ]; then + if [ "$mode" != "expert" ] + then + printf "Host name or ip address of management server [Required]: " + read host + if [ "$host" == "" ]; then + printf "ERROR: Host is required\n" + exit 23; + fi + fi +fi + +port= +if [ "$mode" != "expert" ] +then + printf "Port number of management server [defaults to 8250]: " + read port +fi +if [ "$port" == "" ] +then + port=8250 +fi + +if [ "$zone" == "" ]; then + if [ "$mode" != "expert" ]; then + printf "Availability Zone [Required]: " + read zone + if [ "$zone" == "" ]; then + printf "ERROR: Zone is required\n"; + exit 21; + fi + fi +fi + +if [ "$pod" == "" ]; then + if [ "$mode" != "expert" ]; then + printf "Pod [Required]: " + read pod + if [ "$pod" == "" ]; then + printf "ERROR: Pod is required\n"; + exit 22; + fi + fi +fi + +workers= +if [ "$mode" != "expert" ]; then + printf "# of workers to start [defaults to 3]: " + read workers +fi +if [ "$workers" == "" ]; then + workers=3 +fi + +if [ "$deploydir" == "" ]; then + if [ "$mode" != "expert" ]; then + printf "Directory to deploy to [defaults to /usr/local/vmops/agent]: " + read deploydir + fi + if [ "$deploydir" == "" ]; then + deploydir="/usr/local/vmops/agent" + fi +fi +if ! mkdir -p $deploydir +then + printf "ERROR: Unable to create $deploydir\n" + exit 5 +fi + +if [ "$zipfile" == "" ]; then + if [ "$mode" != "expert" ]; then + printf "Path of the zip file [defaults to agent.zip]: " + read zipfile + fi + if [ "$zipfile" == "" ]; then + zipfile="agent.zip" + fi + +fi +if ! unzip -o $zipfile -d $deploydir +then + printf "ERROR: Unable to unzip $zipfile to $deploydir\n" + exit 6 +fi + +#if ! chmod -R +x $deploydir/scripts/*.sh +#then +# printf "ERROR: Unable to change scripts to executable.\n" +# exit 7 +#fi +#if ! chmod -R +x $deploydir/scripts/iscsi/*.sh +#then +# printf "ERROR: Unable to change scripts to executable.\n" +# exit 8 +#fi +#if ! chmod -R +x $deploydir/*.sh +#then +# printf "ERROR: Unable to change scripts to executable.\n" +# exit 9 +#fi + +if [ "$mode" == "setup" ]; then + mode="expert" + deploydir="/usr/local/vmops/agent" + confdir="/etc/vmops" + /bin/cp -f $deploydir/conf/agent.properties $confdir/agent.properties + if [ $? -gt 0 ]; then + printf "ERROR: Failed to copy the agent.properties file into the right place." + exit 10; + fi +else + confdir="$deploydir/conf" +fi + +if [ "$typ" != "" ]; then + sed s/@TYPE@/"$typ"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: Type is not set\n" +fi + +if [ "$host" != "" ]; then + sed s/@HOST@/"$host"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: host is not set\n" +fi + +if [ "$port" != "" ]; then + sed s/@PORT@/"$port"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: Port is not set\n" +fi + +if [ "$pod" != "" ]; then + sed s/@POD@/"$pod"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: Pod is not set\n" +fi + +if [ "$zone" != "" ]; then + sed s/@ZONE@/"$zone"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: Zone is not set\n" +fi + +if [ "$workers" != "" ]; then + sed s/@WORKERS@/"$workers"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: Workers is not set\n" +fi + +printf "SUCCESS: Installation is now complete. If you like to make changes, edit $confdir/agent.properties\n" +exit 0 diff --git a/build/deploy/deploy-console-proxy.sh b/build/deploy/deploy-console-proxy.sh new file mode 100644 index 00000000000..fe8caf9d09f --- /dev/null +++ b/build/deploy/deploy-console-proxy.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Deploy console proxy package to an existing VM template +# +usage() { + printf "Usage: %s: -d [work directory to deploy to] -z [zip file]" $(basename $0) >&2 +} + +deploydir= +zipfile= + +#set -x + +while getopts 'd:z:' OPTION +do + case "$OPTION" in + d) deploydir="$OPTARG" + ;; + z) zipfile="$OPTARG" + ;; + ?) usage + exit 2 + ;; + esac +done + +printf "NOTE: You must have root privileges to install and run this program.\n" + +if [ "$deploydir" == "" ]; then + printf "ERROR: Unable to find deployment work directory $deploydir\n" + exit 3; +fi +if [ ! -f $deploydir/consoleproxy.tar.gz ] +then + printf "ERROR: Unable to find existing console proxy template file (consoleproxy.tar.gz) to work on at $deploydir\n" + exit 5 +fi + +if [ "$zipfile" == "" ]; then + zipfile="console-proxy.zip" +fi + +if ! mkdir -p /mnt/consoleproxy +then + printf "ERROR: Unable to create /mnt/consoleproxy for mounting template image\n" + exit 5 +fi + +tar xvfz $deploydir/consoleproxy.tar.gz -C $deploydir +mount -o loop $deploydir/vmi-root-fc8-x86_64-domP /mnt/consoleproxy + +if ! unzip -o $zipfile -d /mnt/consoleproxy/usr/local/vmops/consoleproxy +then + printf "ERROR: Unable to unzip $zipfile to $deploydir\n" + exit 6 +fi + +umount /mnt/consoleproxy + +pushd $deploydir +tar cvf consoleproxy.tar vmi-root-fc8-x86_64-domP + +mv -f consoleproxy.tar.gz consoleproxy.tar.gz.old +gzip consoleproxy.tar +popd + +if [ ! -f $deploydir/consoleproxy.tar.gz ] +then + mv consoleproxy.tar.gz.old consoleproxy.tar.gz + printf "ERROR: failed to deploy and recreate the template at $deploydir\n" +fi + +printf "SUCCESS: Installation is now complete. please go to $deploydir to review it\n" +exit 0 diff --git a/build/deploy/deploy-server.sh b/build/deploy/deploy-server.sh new file mode 100755 index 00000000000..1ee9addb3cc --- /dev/null +++ b/build/deploy/deploy-server.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# deploy.sh -- deploys a management server +# +# + +usage() { + printf "Usage: %s: -d [tomcat directory to deploy to] -z [zip file to use]\n" $(basename $0) >&2 +} + +dflag= +zflag= +tflag= +iflag= + +deploydir= +zipfile="client.zip" +typ= + +#set -x + +while getopts 'd:z:x:h:' OPTION +do + case "$OPTION" in + d) dflag=1 + deploydir="$OPTARG" + ;; + z) zflag=1 + zipfile="$OPTARG" + ;; + h) iflag="$OPTARG" + ;; + ?) usage + exit 2 + ;; + esac +done + +if [ "$deploydir" == "" ] +then + if [ "$CATALINA_HOME" == "" ] + then + printf "Tomcat Directory to deploy to: " + read deploydir + else + deploydir="$CATALINA_HOME" + fi +fi + +if [ "$deploydir" == "" ] +then + printf "Tomcat directory was not specified\n"; + exit 15; +fi + +printf "Check to see if the Tomcat directory exist: $deploydir\n" +if [ ! -d $deploydir ] +then + printf "Tomcat directory does not exist\n"; + exit 16; +fi + +if [ "$zipfile" == "" ] +then + printf "Path of the zip file [defaults to client.zip]: " + read zipfile + if [ "$zipfile" == "" ] + then + zipfile="client.zip" + fi +fi +if ! unzip -o $zipfile client.war +then + exit 6 +fi + +rm -fr $deploydir/webapps/client + +if ! unzip -o ./client.war -d $deploydir/webapps/client +then + exit 10; +fi + +rm -f ./client.war + +if ! unzip -o $zipfile lib/* -d $deploydir +then + exit 11; +fi + +if ! unzip -o $zipfile conf/* -d $deploydir +then + exit 12; +fi + +if ! unzip -o $zipfile bin/* -d $deploydir +then + exit 13; +fi + +printf "Adding the conf directory to the class loader for tomcat\n" +sed 's/shared.loader=$/shared.loader=\$\{catalina.home\},\$\{catalina.home\}\/conf\ +/' $deploydir/conf/catalina.properties > $deploydir/conf/catalina.properties.tmp +mv $deploydir/conf/catalina.properties.tmp $deploydir/conf/catalina.properties + +printf "Installation is now complete\n" +exit 0 diff --git a/build/deploy/deploy-simulator.sh b/build/deploy/deploy-simulator.sh new file mode 100644 index 00000000000..f956d64e91a --- /dev/null +++ b/build/deploy/deploy-simulator.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# install.sh -- installs an agent +# +# + +usage() { + printf "Usage: %s: -d [directory to deploy to] -z [zip file] -h [host] -p [pod] -c [data center] -m [expert|novice|setup]\n" $(basename $0) >&2 +} + +mode= +host= +pod= +zone= + +deploydir= +confdir= +zipfile= +typ= + +#set -x + +while getopts 'd:z:x:m:h:p:c:' OPTION +do + case "$OPTION" in + d) deploydir="$OPTARG" + ;; + z) zipfile="$OPTARG" + ;; + m) mode="$OPTARG" + ;; + h) host="$OPTARG" + ;; + p) pod="$OPTARG" + ;; + c) zone="$OPTARG" + ;; + ?) usage + exit 2 + ;; + esac +done + +printf "NOTE: You must have root privileges to install and run this program.\n" + +if [ "$mode" == "setup" ]; then + mode="expert" + deploydir="/usr/local/vmops/agent-simulator" + confdir="/etc/vmops" + /bin/cp -f $deploydir/conf/agent.properties $confdir/agent.properties + if [ $? -gt 0 ]; then + printf "ERROR: Failed to copy the agent.properties file into the right place." + exit 10; + fi +else + confdir="$deploydir/conf" +fi + +if [ "$host" == "" ]; then + if [ "$mode" != "expert" ] + then + printf "Host name or ip address of management server [Required]: " + read host + if [ "$host" == "" ]; then + printf "ERROR: Host is required\n" + exit 23; + fi + fi +fi + +port= +if [ "$mode" != "expert" ] +then + printf "Port number of management server [defaults to 8250]: " + read port +fi +if [ "$port" == "" ] +then + port=8250 +fi + +if [ "$zone" == "" ]; then + if [ "$mode" != "expert" ]; then + printf "Availability Zone [Required]: " + read zone + if [ "$zone" == "" ]; then + printf "ERROR: Zone is required\n"; + exit 21; + fi + fi +fi + +if [ "$pod" == "" ]; then + if [ "$mode" != "expert" ]; then + printf "Pod [Required]: " + read pod + if ["$pod" == ""]; then + printf "ERROR: Pod is required\n"; + exit 22; + fi + fi +fi + +workers= +if [ "$mode" != "expert" ]; then + printf "# of workers to start [defaults to 3]: " + read workers +fi +if [ "$workers" == "" ]; then + workers=3 +fi + +if [ "$deploydir" == "" ]; then + if [ "$mode" != "expert" ]; then + printf "Directory to deploy to [defaults to /usr/local/vmops/agent-simulator]: " + read deploydir + fi + if [ "$deploydir" == "" ]; then + deploydir="/usr/local/vmops/agent-simulator" + fi +fi +if ! mkdir -p $deploydir +then + printf "ERROR: Unable to create $deploydir\n" + exit 5 +fi + +if [ "$zipfile" == "" ]; then + if [ "$mode" != "expert" ]; then + printf "Path of the zip file [defaults to agent-simulator.zip]: " + read zipfile + fi + if [ "$zipfile" == "" ]; then + zipfile="agent-simulator.zip" + fi + +fi +if ! unzip -o $zipfile -d $deploydir +then + printf "ERROR: Unable to unzip $zipfile to $deploydir\n" + exit 6 +fi + +if ! chmod +x $deploydir/*.sh +then + printf "ERROR: Unable to change scripts to executable.\n" + exit 9 +fi + +if [ "$host" != "" ]; then + sed s/@HOST@/"$host"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: host is not set\n" +fi + +if [ "$port" != "" ]; then + sed s/@PORT@/"$port"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: Port is not set\n" +fi + +if [ "$pod" != "" ]; then + sed s/@POD@/"$pod"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: Pod is not set\n" +fi + +if [ "$zone" != "" ]; then + sed s/@ZONE@/"$zone"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: Zone is not set\n" +fi + +if [ "$workers" != "" ]; then + sed s/@WORKERS@/"$workers"/ $confdir/agent.properties > $confdir/tmp + /bin/mv -f $confdir/tmp $confdir/agent.properties +else + printf "INFO: Workers is not set\n" +fi + +printf "SUCCESS: Installation is now complete. If you like to make changes, edit $confdir/agent.properties\n" +exit 0 diff --git a/build/deploy/install-storage-server.sh b/build/deploy/install-storage-server.sh new file mode 100755 index 00000000000..62d16737961 --- /dev/null +++ b/build/deploy/install-storage-server.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# install-storage-server.sh: Installs a VMOps Storage Server +# + +choose_correct_filename() { + local default_filename=$1 + local user_specified_filename=$2 + + if [ -f "$user_specified_filename" ] + then + echo $user_specified_filename + return 0 + else + if [ -f "$default_filename" ] + then + echo $default_filename + return 0 + else + echo "" + return 1 + fi + fi +} + +install_opensolaris_package() { + pkg_name=$1 + + pkg info $pkg_name >> /dev/null + + if [ $? -gt 0 ] + then + # The package is not installed, so install it + pkg install $pkg_name + return $? + else + # The package is already installed + return 0 + fi +} + +exit_if_error() { + return_code=$1 + msg=$2 + + if [ $return_code -gt 0 ] + then + echo $msg + exit 1 + fi +} + +usage() { + printf "Usage: ./install-storage-server.sh " +} + +AGENT_FILE=$(choose_correct_filename "./agent.zip" $1) +exit_if_error $? "Please download agent.zip to your Storage Server." + +TEMPLATES_FILE=$(choose_correct_filename "./templates.tar.gz" $2) +exit_if_error $? "Please download templates.tar.gz to your Storage Server." + +VMOPS_DIR="/usr/local/vmops" +AGENT_DIR="/usr/local/vmops/agent" +CONF_DIR="/etc/vmops" +TEMPLATES_DIR="/root/template" + +# Make all the necessary directories if they don't already exist + +echo "Creating VMOps directories..." +for dir in $VMOPS_DIR $CONF_DIR $TEMPLATES_DIR +do + mkdir -p $dir +done + +# Unzip agent.zip to $AGENT_DIR + +echo "Uncompressing and installing VMOps Storage Agent..." +unzip -o $AGENT_FILE -d $AGENT_DIR >> /dev/null + +# Remove agent/conf/agent.properties, since we should use the file in the real configuration directory + +rm $AGENT_DIR/conf/agent.properties + +# Backup any existing VMOps configuration files, if there aren't any backups already + +if [ ! -d $CONF_DIR/BACKUP ] +then + echo "Backing up existing configuration files..." + mkdir -p $CONF_DIR/BACKUP + cp $CONF_DIR/*.properties $CONF_DIR/BACKUP >> /dev/null +fi + +# Copy all the files in storagehdpatch to their proper places + +echo "Installing system files..." +(cd $AGENT_DIR/storagehdpatch; tar cf - .) | (cd /; tar xf -) +exit_if_error $? "There was a problem with installing system files. Please contact VMOps Support." + +# Make vsetup executable +chmod +x /usr/sbin/vsetup + +# Make vmops executable +chmod +x /lib/svc/method/vmops + +# Uncompress the templates and copy them to the templates directory + +echo "Uncompressing templates..." +tar -xzf $TEMPLATES_FILE -C $TEMPLATES_DIR >> /dev/null +exit_if_error $? "There was a problem with uncompressing templates. Please contact VMOps Support." + +# Install the storage-server package, if it is not already installed +echo "Installing OpenSolaris storage server package..." +install_opensolaris_package "storage-server" +exit_if_error $? "There was a problem with installing the storage server package. Please contact VMOps Support." + +echo "Installing COMSTAR..." +install_opensolaris_package "SUNWiscsit" +exit_if_error $? "Unable to install COMSTAR iscsi target. Please contact VMOps Support." + +# Install the SUNWinstall-test package, if it is not already installed + +echo "Installing OpenSolaris test tools package..." +install_opensolaris_package "SUNWinstall-test" +exit_if_error $? "There was a problem with installing the test tools package. Please contact VMOps Support." + +# Print a success message +printf "\nSuccessfully installed the VMOps Storage Server.\n" +printf "Please complete the following steps to configure your networking settings and storage pools:\n\n" +printf "1. Specify networking settings in /etc/vmops/network.properties\n" +printf "2. Run \"vsetup networking\" and then specify disk settings in /etc/vmops/disks.properties\n" +printf "3. Run \"vsetup zpool\" and reboot the machine when prompted.\n\n" + + diff --git a/build/deploy/install.sh b/build/deploy/install.sh new file mode 100644 index 00000000000..450a32bbf04 --- /dev/null +++ b/build/deploy/install.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +# install.sh -- installs MySQL, Java, Tomcat, and the VMOps server + +#set -x +set -e + +EX_NOHOSTNAME=15 +EX_SELINUX=16 + +function usage() { + printf "Usage: %s [path to server-setup.xml]\n" $(basename $0) >&2 + exit 64 +} + +function checkhostname() { + if hostname | grep -qF . ; then true ; else + echo "You need to have a fully-qualified host name for the setup to work." > /dev/stderr + echo "Please use your operating system's network setup tools to set one." > /dev/stderr + exit $EX_NOHOSTNAME + fi +} + +function checkselinux() { +#### before checking arguments, make sure SELINUX is "permissible" in /etc/selinux/config + if /usr/sbin/getenforce | grep -qi enforcing ; then borked=1 ; fi + if grep -i SELINUX=enforcing /etc/selinux/config ; then borked=1 ; fi + if [ "$borked" == "1" ] ; then + echo "SELINUX is set to enforcing, please set it to permissive in /etc/selinux/config" > /dev/stderr + echo "then reboot the machine, after which you can run the install script again." > /dev/stderr + exit $EX_SELINUX + fi +} + +checkhostname +checkselinux + +if [ "$1" == "" ]; then + usage +fi + +if [ ! -f $1 ]; then + echo "Error: Unable to find $1" > /dev/stderr + exit 2 +fi + +#### check that all files exist +if [ ! -f apache-tomcat-6.0.18.tar.gz ]; then + printf "Error: Unable to find apache-tomcat-6.0.18.tar.gz\n" > /dev/stderr + exit 3 +fi + +if [ ! -f MySQL-client-5.1.30-0.glibc23.x86_64.rpm ]; then + printf "Error: Unable to find MySQL-client-5.1.30-0.glibc23.x86_64.rpm\n" > /dev/stderr + exit 4 +fi + +if [ ! -f MySQL-server-5.1.30-0.glibc23.x86_64.rpm ]; then + printf "Error: Unable to find MySQL-server-5.1.30-0.glibc23.x86_64.rpm\n" > /dev/stderr + exit 5 +fi + +if [ ! -f jdk-6u13-linux-amd64.rpm.bin ]; then + printf "Error: Unable to find jdk-6u13-linux-amd64.rpm.bin\n" > /dev/stderr + exit 6 +fi + +#if [ ! -f osol.tar.bz2 ]; then +# printf "Error: Unable to find osol.tar.bz2\n" +# exit 7 +#fi + +if [ ! -f apache-tomcat-6.0.18.tar.gz ]; then + printf "Error: Unable to find apache-tomcat-6.0.18.tar.gz\n" > /dev/stderr + exit 8 +fi + +if [ ! -f vmops-*.zip ]; then + printf "Error: Unable to find vmops install file\n" > /dev/stderr + exit 9 +fi + +if [ ! -f catalina ] ; then + printf "Error: Unable to find catalina initscript\n" > /dev/stderr + exit 10 +fi + +if [ ! -f usageserver ] ; then + printf "Error: Unable to find usageserver initscript\n" > /dev/stderr + exit 11 +fi + +###### install Apache +# if [ ! -d /usr/local/tomcat ] ; then + echo "installing Apache..." + mkdir -p /usr/local/tomcat + tar xfz apache-tomcat-6.0.18.tar.gz -C /usr/local/tomcat + ln -s /usr/local/tomcat/apache-tomcat-6.0.18 /usr/local/tomcat/current +# fi +# if [ ! -f /etc/profile.d/catalinahome.sh ] ; then +# echo "export CATALINA_HOME=/usr/local/tomcat/current" >> /etc/profile.d/catalinahome.sh +# fi +source /etc/profile.d/catalinahome.sh +# if [ ! -f /etc/init.d/catalina ] ; then + cp -f catalina /etc/init.d + /sbin/chkconfig catalina on +# fi + +####### set up usage server as a service +if [ ! -f /ec/init.d/usageserver ] ; then + cp -f usageserver /etc/init.d + /sbin/chkconfig usageserver on +fi + +##### set up mysql +if rpm -q MySQL-server MySQL-client > /dev/null 2>&1 ; then true ; else + echo "installing MySQL..." + yum localinstall --nogpgcheck -y MySQL-*.rpm +fi + +#### install JDK +echo "installing JDK..." +sh jdk-6u13-linux-amd64.rpm.bin +rm -rf /usr/bin/java +ln -s /usr/java/default/bin/java /usr/bin/java + +#### setting up OSOL image +#mkdir -p $CATALINA_HOME/webapps/images +#echo "copying Open Solaris image, this may take a few moments..." +#cp osol.tar.bz2 $CATALINA_HOME/webapps/images + +#### deploying database +unzip -o vmops-*.zip +cd vmops-* +sh deploy-server.sh -d "$CATALINA_HOME" +cd db +sh deploy-db.sh "../../$1" templates.sql + +exit 0 diff --git a/build/deploy/production/agent/storagehdpatch/etc/default/init b/build/deploy/production/agent/storagehdpatch/etc/default/init new file mode 100644 index 00000000000..4ae34767422 --- /dev/null +++ b/build/deploy/production/agent/storagehdpatch/etc/default/init @@ -0,0 +1,38 @@ +# +# Copyright 2005 Sun Microsystems, Inc. All rights reserved. +# Use is subject to license terms. +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License, Version 1.0 only +# (the "License"). You may not use this file except in compliance +# with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# +#ident "%Z%%M% %I% %E% SMI" +# +# This file is /etc/default/init. /etc/TIMEZONE is a symlink to this file. +# This file looks like a shell script, but it is not. To maintain +# compatibility with old versions of /etc/TIMEZONE, some shell constructs +# (i.e., export commands) are allowed in this file, but are ignored. +# +# Lines of this file should be of the form VAR=value, where VAR is one of +# TZ, LANG, CMASK, or any of the LC_* environment variables. value may +# be enclosed in double quotes (") or single quotes ('). +# +TZ=GMT +CMASK=022 +LANG=en_US.UTF-8 diff --git a/build/deploy/production/agent/storagehdpatch/etc/inet/ntp.conf b/build/deploy/production/agent/storagehdpatch/etc/inet/ntp.conf new file mode 100644 index 00000000000..98fce148097 --- /dev/null +++ b/build/deploy/production/agent/storagehdpatch/etc/inet/ntp.conf @@ -0,0 +1,6 @@ +driftfile /var/lib/ntp/ntp.drift + +server 0.pool.ntp.org +server 1.pool.ntp.org +server 2.pool.ntp.org +server 3.pool.ntp.org diff --git a/build/deploy/production/agent/storagehdpatch/etc/nsswitch.conf b/build/deploy/production/agent/storagehdpatch/etc/nsswitch.conf new file mode 100644 index 00000000000..918d8f37e62 --- /dev/null +++ b/build/deploy/production/agent/storagehdpatch/etc/nsswitch.conf @@ -0,0 +1,70 @@ +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# +# Copyright 2007 Sun Microsystems, Inc. All rights reserved. +# Use is subject to license terms. +# +#ident "%Z%%M% %I% %E% SMI" + +# +# /etc/nsswitch.dns: +# +# An example file that could be copied over to /etc/nsswitch.conf; it uses +# DNS for hosts lookups, otherwise it does not use any other naming service. +# +# "hosts:" and "services:" in this file are used only if the +# /etc/netconfig file has a "-" for nametoaddr_libs of "inet" transports. + +# DNS service expects that an instance of svc:/network/dns/client be +# enabled and online. + +passwd: files +group: files + +# You must also set up the /etc/resolv.conf file for DNS name +# server lookup. See resolv.conf(4). For lookup via mdns +# svc:/network/dns/multicast:default must also be enabled. See mdnsd(1M) +hosts: files dns + +# Note that IPv4 addresses are searched for in all of the ipnodes databases +# before searching the hosts databases. +ipnodes: files dns + +networks: files +protocols: files +rpc: files +ethers: files +netmasks: files +bootparams: files +publickey: files +# At present there isn't a 'files' backend for netgroup; the system will +# figure it out pretty quickly, and won't use netgroups at all. +netgroup: files +automount: files +aliases: files +services: files +printers: user files + +auth_attr: files +prof_attr: files +project: files + +tnrhtp: files +tnrhdb: files diff --git a/build/deploy/production/agent/storagehdpatch/etc/ssh/sshd_config b/build/deploy/production/agent/storagehdpatch/etc/ssh/sshd_config new file mode 100644 index 00000000000..2eb6853336a --- /dev/null +++ b/build/deploy/production/agent/storagehdpatch/etc/ssh/sshd_config @@ -0,0 +1,154 @@ +# +# Copyright 2008 Sun Microsystems, Inc. All rights reserved. +# Use is subject to license terms. +# +# Configuration file for sshd(1m) + +# Protocol versions supported +# +# The sshd shipped in this release of Solaris has support for major versions +# 1 and 2. It is recommended due to security weaknesses in the v1 protocol +# that sites run only v2 if possible. Support for v1 is provided to help sites +# with existing ssh v1 clients/servers to transition. +# Support for v1 may not be available in a future release of Solaris. +# +# To enable support for v1 an RSA1 key must be created with ssh-keygen(1). +# RSA and DSA keys for protocol v2 are created by /etc/init.d/sshd if they +# do not already exist, RSA1 keys for protocol v1 are not automatically created. + +# Uncomment ONLY ONE of the following Protocol statements. + +# Only v2 (recommended) +Protocol 2 + +# Both v1 and v2 (not recommended) +#Protocol 2,1 + +# Only v1 (not recommended) +#Protocol 1 + +# Listen port (the IANA registered port number for ssh is 22) +Port 22 + +# The default listen address is all interfaces, this may need to be changed +# if you wish to restrict the interfaces sshd listens on for a multi homed host. +# Multiple ListenAddress entries are allowed. + +# IPv4 only +#ListenAddress 0.0.0.0 +# IPv4 & IPv6 +ListenAddress :: + +# Port forwarding +AllowTcpForwarding no + +# If port forwarding is enabled, specify if the server can bind to INADDR_ANY. +# This allows the local port forwarding to work when connections are received +# from any remote host. +GatewayPorts no + +# X11 tunneling options +X11Forwarding yes +X11DisplayOffset 10 +X11UseLocalhost yes + +# The maximum number of concurrent unauthenticated connections to sshd. +# start:rate:full see sshd(1) for more information. +# The default is 10 unauthenticated clients. +#MaxStartups 10:30:60 + +# Banner to be printed before authentication starts. +#Banner /etc/issue + +# Should sshd print the /etc/motd file and check for mail. +# On Solaris it is assumed that the login shell will do these (eg /etc/profile). +PrintMotd no + +# KeepAlive specifies whether keep alive messages are sent to the client. +# See sshd(1) for detailed description of what this means. +# Note that the client may also be sending keep alive messages to the server. +KeepAlive yes + +# Syslog facility and level +SyslogFacility auth +LogLevel info + +# +# Authentication configuration +# + +# Host private key files +# Must be on a local disk and readable only by the root user (root:sys 600). +HostKey /etc/ssh/ssh_host_rsa_key +HostKey /etc/ssh/ssh_host_dsa_key + +# Length of the server key +# Default 768, Minimum 512 +ServerKeyBits 768 + +# sshd regenerates the key every KeyRegenerationInterval seconds. +# The key is never stored anywhere except the memory of sshd. +# The default is 1 hour (3600 seconds). +KeyRegenerationInterval 3600 + +# Ensure secure permissions on users .ssh directory. +StrictModes yes + +# Length of time in seconds before a client that hasn't completed +# authentication is disconnected. +# Default is 600 seconds. 0 means no time limit. +LoginGraceTime 600 + +# Maximum number of retries for authentication +# Default is 6. Default (if unset) for MaxAuthTriesLog is MaxAuthTries / 2 +MaxAuthTries 6 +MaxAuthTriesLog 3 + +# Are logins to accounts with empty passwords allowed. +# If PermitEmptyPasswords is no, pass PAM_DISALLOW_NULL_AUTHTOK +# to pam_authenticate(3PAM). +PermitEmptyPasswords no + +# To disable tunneled clear text passwords, change PasswordAuthentication to no. +PasswordAuthentication yes + +# Use PAM via keyboard interactive method for authentication. +# Depending on the setup of pam.conf(4) this may allow tunneled clear text +# passwords even when PasswordAuthentication is set to no. This is dependent +# on what the individual modules request and is out of the control of sshd +# or the protocol. +PAMAuthenticationViaKBDInt yes + +# Are root logins permitted using sshd. +# Note that sshd uses pam_authenticate(3PAM) so the root (or any other) user +# maybe denied access by a PAM module regardless of this setting. +# Valid options are yes, without-password, no. +PermitRootLogin yes + +# sftp subsystem +Subsystem sftp /usr/lib/ssh/sftp-server + + +# SSH protocol v1 specific options +# +# The following options only apply to the v1 protocol and provide +# some form of backwards compatibility with the very weak security +# of /usr/bin/rsh. Their use is not recommended and the functionality +# will be removed when support for v1 protocol is removed. + +# Should sshd use .rhosts and .shosts for password less authentication. +IgnoreRhosts yes +RhostsAuthentication no + +# Rhosts RSA Authentication +# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts. +# If the user on the client side is not root then this won't work on +# Solaris since /usr/bin/ssh is not installed setuid. +RhostsRSAAuthentication no + +# Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication. +#IgnoreUserKnownHosts yes + +# Is pure RSA authentication allowed. +# Default is yes +RSAAuthentication yes diff --git a/build/deploy/production/agent/storagehdpatch/etc/system b/build/deploy/production/agent/storagehdpatch/etc/system new file mode 100644 index 00000000000..0ede11570c3 --- /dev/null +++ b/build/deploy/production/agent/storagehdpatch/etc/system @@ -0,0 +1,101 @@ +*ident "%Z%%M% %I% %E% SMI" /* SVR4 1.5 */ +* +* CDDL HEADER START +* +* The contents of this file are subject to the terms of the +* Common Development and Distribution License, Version 1.0 only +* (the "License"). You may not use this file except in compliance +* with the License. +* +* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +* or http://www.opensolaris.org/os/licensing. +* See the License for the specific language governing permissions +* and limitations under the License. +* +* When distributing Covered Code, include this CDDL HEADER in each +* file and include the License file at usr/src/OPENSOLARIS.LICENSE. +* If applicable, add the following below this CDDL HEADER, with the +* fields enclosed by brackets "[]" replaced with your own identifying +* information: Portions Copyright [yyyy] [name of copyright owner] +* +* CDDL HEADER END +* +* +* SYSTEM SPECIFICATION FILE +* + +* moddir: +* +* Set the search path for modules. This has a format similar to the +* csh path variable. If the module isn't found in the first directory +* it tries the second and so on. The default is /kernel /usr/kernel +* +* Example: +* moddir: /kernel /usr/kernel /other/modules + + + +* root device and root filesystem configuration: +* +* The following may be used to override the defaults provided by +* the boot program: +* +* rootfs: Set the filesystem type of the root. +* +* rootdev: Set the root device. This should be a fully +* expanded physical pathname. The default is the +* physical pathname of the device where the boot +* program resides. The physical pathname is +* highly platform and configuration dependent. +* +* Example: +* rootfs:ufs +* rootdev:/sbus@1,f8000000/esp@0,800000/sd@3,0:a +* +* (Swap device configuration should be specified in /etc/vfstab.) + + + +* exclude: +* +* Modules appearing in the moddir path which are NOT to be loaded, +* even if referenced. Note that `exclude' accepts either a module name, +* or a filename which includes the directory. +* +* Examples: +* exclude: win +* exclude: sys/shmsys + + + +* forceload: +* +* Cause these modules to be loaded at boot time, (just before mounting +* the root filesystem) rather than at first reference. Note that +* forceload expects a filename which includes the directory. Also +* note that loading a module does not necessarily imply that it will +* be installed. +* +* Example: +* forceload: drv/foo + + + +* set: +* +* Set an integer variable in the kernel or a module to a new value. +* This facility should be used with caution. See system(4). +* +* Examples: +* +* To set variables in 'unix': +* +* set nautopush=32 +* set maxusers=40 +* +* To set a variable named 'debug' in the module named 'test_module' +* +* set test_module:debug = 0x13 + +* set zfs:zfs_arc_max=0x4002000 +set zfs:zfs_vdev_cache_size=0 diff --git a/build/deploy/production/agent/storagehdpatch/etc/vmops/disks.properties b/build/deploy/production/agent/storagehdpatch/etc/vmops/disks.properties new file mode 100644 index 00000000000..a7f77b3ee85 --- /dev/null +++ b/build/deploy/production/agent/storagehdpatch/etc/vmops/disks.properties @@ -0,0 +1,7 @@ +# Specify disks in this file +# D: Data +# C: Cache +# L: Intent Log +# S: Spare +# U: Unused + diff --git a/build/deploy/production/agent/storagehdpatch/etc/vmops/network.properties b/build/deploy/production/agent/storagehdpatch/etc/vmops/network.properties new file mode 100644 index 00000000000..8e61bdc8e2f --- /dev/null +++ b/build/deploy/production/agent/storagehdpatch/etc/vmops/network.properties @@ -0,0 +1,35 @@ +# Host Settings +hostname= +domain= +dns1= +dns2= + +# Private/Storage Network Settings (required) +storage.ip= +storage.netmask= +storage.gateway= + +# Second Storage Network Settings (optional) +storage.ip.2= +storage.netmask.2= +storage.gateway.2= + +# Datacenter Settings +pod= +zone= +host= +port= + +# Storage Appliance Settings (optional) +# Specify if you would like to use this Storage Server with an external storage appliance) +iscsi.iqn= +iscsi.ip= +iscsi.port= + +# VMOps IQN (optional) +# Specify if you would like to manually change the IQN of the Storage Server's iSCSI target +vmops.iqn= + +# MTU (optional) +mtu= + diff --git a/build/deploy/production/agent/storagehdpatch/lib/svc/method/vmops b/build/deploy/production/agent/storagehdpatch/lib/svc/method/vmops new file mode 100755 index 00000000000..db9fba1dae3 --- /dev/null +++ b/build/deploy/production/agent/storagehdpatch/lib/svc/method/vmops @@ -0,0 +1,106 @@ +#!/bin/bash +# +# vmops Script to start and stop the VMOps Agent. +# +# Author: Chiradeep Vittal +# chkconfig: 2345 99 01 +# description: Start up the VMOps agent + +# Source function library. +if [ -f /etc/init.d/functions ] +then + . /etc/init.d/functions +fi + +_success() { + if [ -f /etc/init.d/functions ] + then + success + else + echo "Success" + fi +} + +_failure() { + if [ -f /etc/init.d/functions ] + then + failure + else + echo "Failed" + fi +} +RETVAL=$? +VMOPS_HOME="/usr/local/vmops" + +mkdir -p /var/log/vmops + +get_pids() { + local i + for i in $(ps -ef | grep agent.sh | grep -v grep | awk '{print $2}'); + do + echo $(pwdx $i) | grep "$VMOPS_HOME" | grep agent | awk -F: '{print $1}'; + done +} + +start() { + local pid=$(get_pids) + echo -n "Starting VMOps agent: " + if [ -f $VMOPS_HOME/agent/agent.sh ]; + then + if [ "$pid" == "" ] + then + (cd $VMOPS_HOME/agent; nohup ./agent.sh > /var/log/vmops/vmops.out 2>&1 & ) + pid=$(get_pids) + echo $pid > /var/run/vmops.pid + fi + _success + else + _failure + fi + echo +} + +stop() { + local pid + echo -n "Stopping VMOps agent: " + for pid in $(get_pids) + do + pgid=$(ps -o pgid -p $pid | tr '\n' ' ' | awk '{print $2}') + pgid=${pgid## } + pgid=${pgid%% } + kill -- -$pgid + done + rm /var/run/vmops.pid + _success + echo +} + +status() { + local pids=$(get_pids) + if [ "$pids" == "" ] + then + echo "VMOps agent is not running" + return 1 + fi + echo "VMOps agent (pid $pids) is running" + return 0 +} + + +case "$1" in + start) start + ;; + stop) stop + ;; + status) status + ;; + restart) stop + sleep 1.5 + start + ;; + *) echo $"Usage: $0 {start|stop|status|restart}" + exit 1 + ;; +esac + +exit $RETVAL diff --git a/build/deploy/production/agent/storagehdpatch/usr/sbin/vsetup b/build/deploy/production/agent/storagehdpatch/usr/sbin/vsetup new file mode 100755 index 00000000000..dbe8a4b014c --- /dev/null +++ b/build/deploy/production/agent/storagehdpatch/usr/sbin/vsetup @@ -0,0 +1,44 @@ +#! /bin/bash + +stage=$1 +option=$2 + +export VMOPS_HOME=/usr/local/vmops + +usage() { + echo "Usage: vsetup [networking|zpool]" + echo " networking: probe NICs, configure networking, and detect disks" + echo " zpool: create ZFS storage pool" +} + +if [ "$stage" != "networking" ] && [ "$stage" != "zpool" ] && [ "$stage" != "detectdisks" ] +then + usage + exit 1 +fi + +if [ "$option" != "" ] && [ "$option" != "-listonly" ] +then + usage + exit 1 +fi + +$VMOPS_HOME/agent/scripts/installer/run_installer.sh storage $stage $option + +if [ $? -eq 0 ] +then + if [ "$stage" == "networking" ] + then + echo "Please edit /etc/vmops/disks.properties and then run \"vsetup zpool\"." + else + if [ "$stage" == "zpool" ] + then + echo "Press enter to reboot the computer..." + read + reboot + fi + fi +fi + + + diff --git a/build/deploy/production/agent/storagehdpatch/var/svc/manifest/application/cloud.xml b/build/deploy/production/agent/storagehdpatch/var/svc/manifest/application/cloud.xml new file mode 100644 index 00000000000..858bb16c7e3 --- /dev/null +++ b/build/deploy/production/agent/storagehdpatch/var/svc/manifest/application/cloud.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/deploy/production/consoleproxy/conf/consoleproxy.properties b/build/deploy/production/consoleproxy/conf/consoleproxy.properties new file mode 100644 index 00000000000..743db37ce54 --- /dev/null +++ b/build/deploy/production/consoleproxy/conf/consoleproxy.properties @@ -0,0 +1,6 @@ +consoleproxy.tcpListenPort=0 +consoleproxy.httpListenPort=80 +consoleproxy.httpCmdListenPort=8001 +consoleproxy.jarDir=./applet/ +consoleproxy.viewerLinger=180 +consoleproxy.reconnectMaxRetry=5 diff --git a/build/deploy/production/db/server-setup-dev.xml b/build/deploy/production/db/server-setup-dev.xml new file mode 100644 index 00000000000..429764a6e3e --- /dev/null +++ b/build/deploy/production/db/server-setup-dev.xml @@ -0,0 +1,532 @@ + + + 2.0 + + + 1 + AH + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 100-199 + 10.1.1.0/24 + + + 2 + KM + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 200-299 + 10.1.1.0/24 + + + 3 + KY + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 300-399 + 10.1.1.0/24 + + + 4 + WC + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 400-499 + 10.1.1.0/24 + + + 5 + CV + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 500-599 + 10.1.1.0/24 + + + 6 + KS + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 600-699 + 10.1.1.0/24 + + + 7 + ES + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 700-799 + 10.1.1.0/24 + + + 8 + RC + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 800-899 + 10.1.1.0/24 + + + 9 + AX + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 900-999 + 10.1.1.0/24 + + + 10 + JW + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 900-999 + 10.1.1.0/24 + + + 11 + AJ + 72.52.126.11 + 72.52.126.12 + 192.168.10.253 + 192.168.10.254 + 1000-1099 + 10.1.1.0/24 + + + + + + + 1 + 31 + VirtualNetwork + 192.168.31.1 + 255.255.255.0 + 192.168.31.150-192.168.31.159 + + + 2 + 32 + VirtualNetwork + 192.168.32.1 + 255.255.255.0 + 192.168.32.150-192.168.32.159 + + + 3 + 33 + VirtualNetwork + 192.168.33.1 + 255.255.255.0 + 192.168.33.150-192.168.33.159 + + + 4 + 34 + VirtualNetwork + 192.168.34.1 + 255.255.255.0 + 192.168.34.150-192.168.34.159 + + + 5 + 35 + VirtualNetwork + 192.168.35.1 + 255.255.255.0 + 192.168.35.150-192.168.35.159 + + + 6 + 36 + VirtualNetwork + 192.168.36.1 + 255.255.255.0 + 192.168.36.150-192.168.36.159 + + + 7 + 37 + VirtualNetwork + 192.168.37.1 + 255.255.255.0 + 192.168.37.150-192.168.37.159 + + + 8 + 38 + VirtualNetwork + 192.168.38.1 + 255.255.255.0 + 192.168.38.150-192.168.38.159 + + + 9 + 39 + VirtualNetwork + 192.168.39.1 + 255.255.255.0 + 192.168.39.150-192.168.39.159 + + + 10 + 40 + VirtualNetwork + 192.168.40.1 + 255.255.255.0 + 192.168.40.150-192.168.40.159 + + + 11 + 41 + VirtualNetwork + 192.168.41.1 + 255.255.255.0 + 192.168.41.150-192.168.41.159 + + + + + + 1 + AH + 1 + 192.168.10.20-192.168.10.24 + 192.168.10.0/24 + + + 2 + KM + 2 + 192.168.10.25-192.168.10.29 + 192.168.10.0/24 + + + 3 + KY + 3 + 192.168.10.30-192.168.10.34 + 192.168.10.0/24 + + + 4 + WC + 4 + 192.168.10.35-192.168.10.39 + 192.168.10.0/24 + + + 5 + CV + 5 + 192.168.10.40-192.168.10.44 + 192.168.10.0/24 + + + 6 + KS + 6 + 192.168.10.45-192.168.10.49 + 192.168.10.0/24 + + + 7 + ES + 7 + 192.168.10.50-192.168.10.54 + 192.168.10.0/24 + + + 8 + RC + 8 + 192.168.10.55-192.168.10.59 + 192.168.10.0/24 + + + 9 + AX + 9 + 192.168.10.62-192.168.10.64 + 192.168.10.0/24 + + + 10 + JW + 10 + 192.168.10.65-192.168.10.69 + 192.168.10.0/24 + + + 11 + AJ + 11 + 192.168.10.70-192.168.10.74 + 192.168.10.0/24 + + + + + + + 1 + Small Instance + Small Instance [500MHZ CPU, 512MB MEM, 16GB Disk] - $0.10 per hour + 1 + 512 + 500 + false + + + 2 + Medium Instance + Medium Instance [500MHZ CPU, 1GB MEM, 32GB Disk] - $0.20 per hour + 1 + 1024 + 512 + + + 3 + Large Instance + Large Instance [2GHZ CPU, 4GB MEM, 64GB Disk] - $0.30 per hour + 2 + 4096 + 2000 + + + + + + 1 + 1 + Small Disk + Small Disk [16GB Disk] + 16000 + + + 2 + 1 + Medium Disk + Medium Disk [32GB Disk] + 32000 + + + 3 + 1 + Large Disk + Large Disk [64GB Disk] + 64000 + + + + + + + 2 + admin + password + Admin + User + admin@mailprovider.com + + + + + + default.zone + AH + + + domain.suffix + cloud-test.cloud.com + + + instance.name + AH + + + consoleproxy.ram.size + 256 + + + host.stats.interval + 3600000 + + + storage.stats.interval + 120000 + + + volume.stats.interval + -1 + + + ping.interval + 60 + + + alert.wait + 1800 + + + expunge.interval + 86400 + + + usage.aggregation.timezone + GMT + + + + ssh.privatekey + -----BEGIN RSA PRIVATE KEY-----\nMIIEoQIBAAKCAQEAnNUMVgQS87EzAQN9ufGgH3T1kOpqcvTmUrp8RVZyeA5qwptS\nrZxONRbhLK709pZFBJLmeFqiqciWoA/srVIFk+rPmBlVsMw8BK53hTGoax7iSe8s\nLFCAATm6vp0HnZzYqNfrzR2by36ET5aQD/VAyA55u+uUgAlxQuhKff2xjyahEHs+\nUiRlReiAgItygm9g3co3+8fJDOuRse+s0TOip1D0jPdo2AJFscyxrG9hWqQH86R/\nZlLJ7DqsiaAcUmn52u6Nsmd3BkRmGVx/D35Mq6upJqrk/QDfug9LF66yiIP/BEIn\n08N/wQ6m/O37WUtqqyl3rRKqs5TJ9ZnhsqeO9QIBIwKCAQA6QIDsv69EkkYk8qsK\njPJU06uq2rnS7T+bEhDmjdK+4MiRbOQx2vh6HnDktgM3BJ1K13oss/NGYHJ190lH\nsMA+QUXKx5TbRItSMixkrAta/Ne1D7FSScklBtBVbYZ8XtQhdMVML5GjWuCv2NZs\nU8eaw4xNHPyklcr7mBurI7b6p13VK5BNUWR/VNuigT4U89YzRcoEZ/sTlR+4ACYr\nxbUJJGBA03+NhdSAe2vodlMh5lGflD0JmHMFqqg9BcAtVb73JsOsxFQArbXwRd/q\nNckdoAvgJfhTOvXF5GMPLI0lGb6skJkS229F4GaBB2Iz4A9O0aHZob8I8zsWUbiu\npvBrAoGBAMjUDfF2x13NjH1cFHietO5O1oM0nZaAxKodxoAUvHVMUd5DIY50tqYw\n7ecKi2Cw43ONpdj0nP9Nc2NV3NDRqLopwkKUsTtq9AKQ2cIuw3+uS5vm0VZBzmTP\nuF04Qo4bXh/jFRA62u9bXsmIFtaehKxE1Gp6zi393GcbWP4HX/3dAoGBAMfq0KD3\ngeU1PHi9uI3Ss89nXzJsiGcwC5Iunu1aTzJCYhMlJkfmRcXYMAqSfg0nGWnfvlDh\nuOO26CHKjG182mTwYXdgQzIPpBc8suvgUWDBTrIzJI+zuyBLtPbd9DJEVrZkRVQX\nXrOV3Y5oOWsba4F+b20jaaHFAiY7s6OtrX/5AoGBAMMXI3zZyPwJgSlSIoPNX03m\nL3gke9QID4CvNduB26UlkVuRq5GzNRZ4rJdMEl3tqcC1fImdKswfWiX7o06ChqY3\nMb0FePfkPX7V2tnkSOJuzRsavLoxTCdqsxi6T0g318c0XZq81K4A/P5Jr8ksRl40\nPA+qfyVdAf3Cy3ptkHLzAoGASkFGLSi7N+CSzcLPhSJgCzUGGgsOF7LCeB/x4yGL\nIUvbSPCKj7vuB6gR2AqGlyvHnFprQpz7h8eYDI0PlmGS8kqn2+HtEpgYYGcAoMEI\nSIJQbhL+84vmaxTOL87IanEnhZL1LdzLZ0ZK+mE55fQ936P9gE77WVfNmSweJtob\n3xMCgYAl0aLeGf4oUZbI56eEaCbu8U7dEe6MF54VbozyiXqbp455QnUpuBrRn5uf\nc079dNcqTNDuk1+hYX9qNn1aXsvWeuofBXqWoFXu/c4yoWxJAPhEVhzZ9xrXI76I\nBKiPCyKrOa7bSLvs6SQPpuf5AQ8+NJrOxkEB9hbMuaAr2N5rCw==\n-----END RSA PRIVATE KEY----- + + Hidden + + + ssh.publickey + + ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAnNUMVgQS87EzAQN9ufGgH3T1kOpqcvTmUrp8RVZyeA5qwptSrZxONRbhLK709pZFBJLmeFqiqciWoA/srVIFk+rPmBlVsMw8BK53hTGoax7iSe8sLFCAATm6vp0HnZzYqNfrzR2by36ET5aQD/VAyA55u+uUgAlxQuhKff2xjyahEHs+UiRlReiAgItygm9g3co3+8fJDOuRse+s0TOip1D0jPdo2AJFscyxrG9hWqQH86R/ZlLJ7DqsiaAcUmn52u6Nsmd3BkRmGVx/D35Mq6upJqrk/QDfug9LF66yiIP/BEIn08N/wQ6m/O37WUtqqyl3rRKqs5TJ9ZnhsqeO9Q== root@test2.lab.vmops.com + + Hidden + + + + + memory.capacity.threshold + 0.85 + + + cpu.capacity.threshold + 0.85 + + + storage.capacity.threshold + 0.85 + + + storage.allocated.capacity.threshold + 0.85 + + + capacity.check.period + 3600000 + + + wait + 240 + + + network.throttling.rate + 200 + + + multicast.throttling.rate + 10 + + + + + + + diff --git a/build/deploy/production/db/templates-dev.sql b/build/deploy/production/db/templates-dev.sql new file mode 100644 index 00000000000..a12d5e14bbe --- /dev/null +++ b/build/deploy/production/db/templates-dev.sql @@ -0,0 +1,14 @@ +INSERT INTO `vmops`.`vm_template` (id, unique_name, name, public, path, created, type, hvm, bits, created_by, url, checksum, ready, display_text, enable_password) + VALUES (1, 'routing', 'DomR Template', 0, 'tank/volumes/demo/template/private/u000000/os/routing', now(), 'ext3', 0, 64, 1, 'http://vmopsserver.lab.vmops.com/images/routing/vmi-root-fc8-x86_64-domR.img.bz2', 'd00927f863a23b98cc6df6e377c9d0c6', 0, 'DomR Template', 0); +INSERT INTO `vmops`.`vm_template` (id, unique_name, name, public, path, created, type, hvm, bits, created_by, url, checksum, ready, display_text, enable_password) + VALUES (3, 'centos53-x86_64', 'Centos 5.3(x86_64) no GUI', 1, 'tank/volumes/demo/template/public/os/centos53-x86_64', now(), 'ext3', 0, 64, 1, 'http://vmopsserver.lab.vmops.com/images/centos52-x86_64/vmi-root-centos.5-2.64.pv.img.gz', 'd4ca80825d936db00eedf26620f13d69', 0, 'Centos 5.3(x86_64) no GUI', 0); +#INSERT INTO `vmops`.`vm_template` (id, unique_name, name, public, path, created, type, hvm, bits, created_by, url, checksum, ready, display_text, enable_password) +# VALUES (4, 'centos52-x86_64-gui', 'Centos 5.2(x86_64) GUI', 1, 'tank/volumes/demo/template/public/os/centos52-x86_64-gui', now(), 'ext3', 0, 64, 1, 'http://vmopsserver.lab.vmops.com/images/centos52-x86_64/vmi-root-centos.5-2.64.pv.img.gz', 'd4ca80825d936db00eedf26620f13d69', 0, 'Centos 5.2(x86_64) GUI', 0); +INSERT INTO `vmops`.`vm_template` (id, unique_name, name, public, path, created, type, hvm, bits, created_by, url, checksum, ready, display_text, enable_password) + VALUES (5, 'winxpsp3', 'Windows XP SP3 (32-bit)', 1, 'tank/volumes/demo/template/public/os/winxpsp3', now(), 'ntfs', 1, 32, 1, 'http://vmopsserver.lab.vmops.com/images/fedora10-x86_64/vmi-root-fedora10.64.img.gz', 'c76d42703f14108b15acc9983307c759', 0, 'Windows XP SP3 (32-bit)', 0); +INSERT INTO `vmops`.`vm_template` (id, unique_name, name, public, path, created, type, hvm, bits, created_by, url, checksum, ready, display_text, enable_password) + VALUES (7, 'win2003sp2', 'Windows 2003 SP2 (32-bit)', 1, 'tank/volumes/demo/template/public/os/win2003sp2', now(), 'ntfs', 1, 32, 1, 'http://vmopsserver.lab.vmops.com/images/win2003sp2/vmi-root-win2003sp2.img.gz', '4d2cc51898d05c0f7a2852c15bcdc77b', 0, 'Windows 2003 SP2 (32-bit)', 0); +INSERT INTO `vmops`.`vm_template` (id, unique_name, name, public, path, created, type, hvm, bits, created_by, url, checksum, ready, display_text, enable_password) + VALUES (8, 'win2003sp2-x64', 'Windows 2003 SP2 (64-bit)', 1, 'tank/volumes/demo/template/public/os/win2003sp2-x64', now(), 'ntfs', 1, 64, 1, 'http://vmopsserver.lab.vmops.com/images/win2003sp2-x86_64/vmi-root-win2003sp2-x64.img.gz', '35d4de1c38eb4fb9d81a31c1d989c482', 0, 'Windows 2003 SP2 (64-bit)', 0); +INSERT INTO `vmops`.`vm_template` (id, unique_name, name, public, path, created, type, hvm, bits, created_by, url, checksum, ready, display_text, enable_password) + VALUES (9, 'fedora12-GUI-x86_64', 'Fedora 12 Desktop(64-bit)', 1, 'tank/volumes/demo/template/public/os/fedora12-GUI-x86_64', now(), 'ext3', 1, 64, 1, 'http://vmopsserver.lab.vmops.com/images/fedora12-GUI-x86_64/vmi-root-fedora12-GUI-x86_64.qcow2.gz', '', 0, 'Fedora 12 Desktop (with httpd,java and mysql)', 0); diff --git a/build/deploy/production/premium/conf/log4j-cloud_usage.xml b/build/deploy/production/premium/conf/log4j-cloud_usage.xml new file mode 100644 index 00000000000..46500faad81 --- /dev/null +++ b/build/deploy/production/premium/conf/log4j-cloud_usage.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/deploy/production/premium/conf/log4j-cloud_usage.xml.template b/build/deploy/production/premium/conf/log4j-cloud_usage.xml.template new file mode 100644 index 00000000000..7c94e594c93 --- /dev/null +++ b/build/deploy/production/premium/conf/log4j-cloud_usage.xml.template @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/deploy/production/premium/conf/usage-components.xml b/build/deploy/production/premium/conf/usage-components.xml new file mode 100644 index 00000000000..5e178302819 --- /dev/null +++ b/build/deploy/production/premium/conf/usage-components.xml @@ -0,0 +1,48 @@ + + + + + + + + 50 + -1 + + + + + + + + + + + + + + + DAILY + + + diff --git a/build/deploy/production/server/conf/agent-update.properties b/build/deploy/production/server/conf/agent-update.properties new file mode 100644 index 00000000000..6b42aa8b6f5 --- /dev/null +++ b/build/deploy/production/server/conf/agent-update.properties @@ -0,0 +1 @@ +agent.minimal.version=@agent.min.version@ diff --git a/build/deploy/production/server/conf/cloud-localhost.pk12 b/build/deploy/production/server/conf/cloud-localhost.pk12 new file mode 100644 index 0000000000000000000000000000000000000000..79dfc4d7aa6f791c251561121d05b2079a59e008 GIT binary patch literal 1597 zcmY+?do&XY90zdQY>au8DAJ6G6}CiJVPg@MXGUJJ8WBq@6k#Pdk2J5c%%v5PlGmk0 z(L8F&%3I-@W{JrotSIg}=ib}BfBeqxobUJiet&&XRERkc076m0>kugNiv1Ol96$dYX?sm#ze1Z`4BfyWevO!@r9efmBQ7 z_M(cNTBVDlgoI`oUV!m5d|Ob&118<+hEoCsS=p)G#mPoa4$Np=FYEE*WU0YU3O)w;8E6~j$`J)7xoX2++I(lD?vGf7f;#h_oLs}>4CTF zeWFr@V>c<&e8hIfrPiA`(^_v0B54puCT08*19tdPF%RK!t1o>;LmFfK5Q_bd!E?9) z=OnOD^L72jHe*d^qzH>J6lGnIvRZ0@g$DP3;M^OMIIRT2k|*<52yI<73#YGiuH7I+@^xLp!l-&emF;&*8P_ zlvuG^s5dTrUJXv2^|mwngg49HmdG!Ak@DxqiiQ41NMkjW-iL`<)$Q4hu~&jCnHgs- zJ6W@{r5PQ+KQ7p_a_tN_e#?GTSz!!Izn5*PxAQA9krqqjzI`-RZQZ%StV$#A*nmQ^ z9uC?^=Pjto+EWOb*#ZRhJcQIACA&6>C~hJ(NwO#>9A<{q@6CrD5j7u?d~J@VXR^7W1&OsD|%wk z-~IzyOnpj{3KWRraq)~oWdGqU0|1K45^;F@f9gR$)l;f<%RH*oXMU;|pC=X=*|+3* zS(Yv}UEFLzwuq_gB-hP)D6bu5rYXT~QjUbuwf|_h2{oI;st%V<;_Wpv-(A*f8Qn$* zH^p9qX+1l!3cYL4n@$}UYJ&^W6O6iZauET=G11(n;jDSXBXI*!%!!(^glv0BEp;=B zjB9J_3)xSm4=;={8SdQtsEbD-zSs3_q zgy?fQTuU3)b|N0ikA-odS0i|qWf!m~zN;qAJCsm-=z1OvJ`uhSlX(bM7oc`I^} zWnd@3Wln_GKY>cN@WyN2qhttLQGRM zwv{*FHrqqM*+Q0fI51i-aZ0WeyHsC3y%JbJ8#Lc-k31f?gzIvwi5Z9scHyfn$K|{u zg4fpFf%z?C2=ntJ$2Yx*o~_x0@Aq@0Gux11*Ih2>Z6pBMqc!uV!#BXxpICbu0|@QY z?M%-TW8Z3ZBZL}1*!?X*wE;~%PvU`WoX)|a7xxw4=;cmSPKRqB^$E-*EmC$h*zI!d zdU>O8+{B-biE7&-`}FH@-vUVfs@=7mHHWDRUF|(gOc>%CM1Utt6CM*l-CaS%=!e2- zgN>>Vf$nG>l=i=>p_|YulFDi;YKp0i4<=I5BNWrXrVk60r_)eq6aoc>fOpD+fNEd~ mNoB%28Bs_hv~YGf@XgfP?yK7I7TuE4a6VE8+nL??Q} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/deploy/production/server/conf/log4j-cloud.xml b/build/deploy/production/server/conf/log4j-cloud.xml new file mode 100755 index 00000000000..2142d81eaaa --- /dev/null +++ b/build/deploy/production/server/conf/log4j-cloud.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/deploy/production/server/conf/log4j-cloud.xml.template b/build/deploy/production/server/conf/log4j-cloud.xml.template new file mode 100644 index 00000000000..5e1e6598884 --- /dev/null +++ b/build/deploy/production/server/conf/log4j-cloud.xml.template @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/deploy/production/server/conf/server.xml b/build/deploy/production/server/conf/server.xml new file mode 100755 index 00000000000..99ea003791c --- /dev/null +++ b/build/deploy/production/server/conf/server.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/developer.xml b/build/developer.xml new file mode 100755 index 00000000000..9bb2d33e6e3 --- /dev/null +++ b/build/developer.xml @@ -0,0 +1,187 @@ + + + + + + + This is a developer.xml with tools to start and stop tomcat and + generally developer targets that has nothing to do with compiling. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/overview.html b/build/overview.html new file mode 100755 index 00000000000..5837e5e2841 --- /dev/null +++ b/build/overview.html @@ -0,0 +1,18 @@ + + + + + + + +

+ +VMOps source javadoc. + +

+Javadoc for JUnit tests +is also available. +

+ + + diff --git a/build/package.xml b/build/package.xml new file mode 100755 index 00000000000..2476eea3bbe --- /dev/null +++ b/build/package.xml @@ -0,0 +1,245 @@ + + + + + + + This is a package.xml with tools to package the cloud stack distribution + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/release-notes b/build/release-notes new file mode 100644 index 00000000000..5523568e72a --- /dev/null +++ b/build/release-notes @@ -0,0 +1,328 @@ +******************************************************************************** + VMOps Cloud Stack Version 0.4 + Release Notes +******************************************************************************** + +================================= +WHAT'S NEW : +================================= + +* NIC bonding support for the Computing, Routing, and Storage nodes to take +advantage of the multiple NICS installed on the server. +* Maintenance Mode support for physical servers. Administrators now have the +option to enable or disable maintenance mode on any physical servers. Enabling +maintenance mode on a Routing or Computing node will result in the seamless live +migration of guest virtual machines into other physical servers within the same +zone before making the server available for maintenance. +* Introduction of a new user web interface for allowing user accounts to manage +their own virtual machines. This new interface has a brand new look and feel, +allows for easier branding and customization, and is easier to incorporate into +existing web interfaces. +* Added support for the creation of Reseller Domains. The Reseller Domain +feature allows host providers to create resellers complete with their own +user base and administrators while using the same global computing resources +provided by the host provider. +* Added a new email alerting system that will now inform administrators of +events such as when physical servers are down or computing resources are +passing a pre-configured threshold. +* Massive improvements to the existing Developer and Integration API. Error +codes have now been added and existing API method names and parameters have been +made more consistent. A JSON format can also now be optionally +returned in addition to the existing XML format. Please read the new API +Reference documentation for more details on the changes. +* Billing records have now been changed to Usage records to provide data for +both billing integration and usage metering. Price has been completely removed +from VMOps. Instead we added ability to set your own display text for both +service offering and templates. You can now use this to set any text for the UI +to display. +* New virtual machines deployed will now generate a random password. A reset +password is also now available both in the web user interface as well as the +API. We support both Linux and Windows OS based templates. +* Storage server is now a bare-bone installation and uses the new COMSTAR iSCSI +stack for improved performance. We recognized that while running the storage +server as a virtual machine does allows for more hardware support, it severely +impacts performance. The list of compatible hardware can be found in the Open +Solaris website. +* Added clustering support for the VMOps Management Server. +* Added the ability to configure an additional storage ip address (separate +subnet) for both Routing and Computing servers. If a server is configured with +an additional storage ip, all storage traffic will route through this subnet and +all network/internet traffic will route through the existing private network. +* Added concept of a user account. VMOps supports multiple username for a +single user account. +* Created new installers for the VMOps MultiTenant Hypervisor and the VMOps +Storage. + +================================= +KNOWN ISSUES : +================================= + +* DomR is still counting internal network activities as part of the usage that +is being returned by the API. +* The reset password for virtual machine feature does not return an error if it +fails to successfully reset the password. +* VMOps installation scripts to not validate bad network configuration values. +* VNX Proxy Server does not handle rapid refreshes of web console proxy well. +* VNC Proxy Server at times do not return a thumbnail image. +* Rebooting a DomR instance will cause network traffic to not be collected. +* Associating new IP addresses should clean out existing LB or PF rules before +assigning it to a DomR instance. +* The Usage parse engine needs to be split out from the VMOps Management Server +so that only a single instance of this can be running and does not affect +normal operations. +* Templates needs a way of specifying the minimum CPU and Memory requirements. +* createUser API method currently allows you to assign users to admin accounts. +* Installations of servers with more than 3 NIC sometimes duplicates the MAC +address on the ifcfg configuration files. +* Additional admin only API methods are missing (ie. Domain management, router +management). +* Usage parse engine could go OOM in the event it has not been run recently +and there are a large (2 million+ records). +* Problem with domU when both e1000 and e1000e used as a NIC drivers for a +Computing Server. The installer needs to blacklist one of the drivers. +* vnet failures and xen crashes currently do not generate an alert to the administrator. +* The current limit for domU instances created on a Computing Server is 64 and +the currently limit for domR instances created is 59 on a Routing Server. +* No current way of allowing different subnets for different pods within the +same zone. +* limit the number of usage and event records returned by the API. A large +enough of the query could cause the Management Server to go oom. + +================================= +BUG FIXES : +================================= + +* Improved transactions across both database calls and calls across agents. +* Fixed an issue where duplicate IP or LB rules could be sent to the DomR +instance during a DomR restart. +* Removed requirement of the reverse DNS lookup for the Storage Server. +* Massive improvements to the HA process. +* Fixed an issue where the it would take too long for the management server +to detect a TCP disconnect. +* Fixed an issue where the the agent would *think* it has connected to the +management server but in reality, it is just stuck waiting for a response that +will never come. +* Generic DB lock wait timeout fixes. +* Improvements to the general state management of the servers. +* Fixed issue where where physical servers with the same IP attempts to connect +to the Management Server. The second server is now prevented from registering. +* Fixed an issue where deleting a user from an account would result in all the +virtual machines to be cleaned up. This can only happen if the last user for +an account has been deleted. +* Fixed an issue where the source NAT ip address of a DomR instance is being +released back into the public pool even though the DomR instance was not +successfully destroyed. +* Fixed an issue where a guest virtual machine cannot be destroyed while in HA +state. +* Removed requirement to specify the storage type when installing a new tempate. +* Fixed an issue where the console proxy from different zones are all starting +in the same zone. +* Fixed an issue where the listing of virtual machines would hang if the console +proxy is not even started. +* Massive improvements to our installer scripts. +* Massive improvements to the general stability of the Cloud Stack. +* Fixed an issue where the Hypervisor installer is unable to install onto +machines with a IPMI card. +* As usual, there are too many bug fixes to list... + +******************************************************************************** + VMOps Cloud Stack Version 0.3.xxxx + Release Notes +******************************************************************************** + +================================= +WHAT'S NEW : +================================= + +* Introduction of VMOps Developer API. This allows users of the VMOps Cloud + Stack to manage their virtual machines through a set of REST-like API. +* Improved collection of user usage data that can be used for both billing + aggregation and metric collection. +* High availability support for user virtual machines. +* Support for automatic hypervisor agent software update. +* VNC console proxy servers can now run as managed VMOps system VMs. The new + implementation provides features such as on-demand VM startup, standby + capacity pre-allocation and console proxy load monitoring. +* Much Improved VMOps Cloud Stack installation process. VMOps Cloud Stack can + now be installed with a minimum of two physical servers. +* VMOps Cloud Stack installation DVD now comes in two flavors. VMOps Cloud Stack + Management Server Installation DVD and VMOps Cloud Stack Multitenant + Hypervisor Installation DVD. + +================================= +KNOWN ISSUES : +================================= + +* PV drivers for Windows 2003 and Windows 2003 x86_64 (Incomplete Feature) +* GUI panel for allowing administrators to set various system configurations - + i.e. zones, pods, ip addresses (Incomplete Feature) +* Support for multiple NIC cards on computing, routing, and storage servers + (Disabled Feature) +* Support for resellers (Incomplete Feature) +* Allow admins/users to specify the root password for their new instance. It + cannot default to root/password (Bug 134) +* Admin/User Dashboard (Bug 154 and 155) +* Dynamically refresh server and vm instance status in GUI without a manual + refresh (Bug 389) +* Need transaction semantics across DB and agent. Without this, it is possible + to timeout db calls that first lock a record and relies on an agent response + before releasing that record. (Bug 408) +* All Server Roles (Mgmt, Computing, Routing, and Storage) require a functioning + eth0 NIC in order to install correctly. (Bug 470) +* Unable to handle HA when an entire Pod is unreachable (Bug 620) +* Improved network traffic reporting (Bug 642) +* Multiple login support a single user account (Bug 589) +* DomR instances continue to run even though all VMs associated with the DomR + are no longer even running. (Bug 617) +* HA fails when VM and Router for the same user go down at the same time + (Bug 603) + +================================= +BUG FIXES : +================================= + +* Improved Billing data generation (Bug 482) +* Able to create new admins through the admin console UI. (Bug 492) +* Able to create new Service Offerings through the admin console UI (Bug 500) +* Significantly improved the imaging speed when installing VMOps Cloud Stack + (Bug 476) +* Harden DomR Template to prevent unauthorized access +* No longer require eth0 during installation process of the hosts (Bug 490) +* Fixed issue where having multiple NIC cards caused issues (Bug 489) +* Installation UI will now allow you to select to the disk for storage + (Bug 556) +* Installation UI will now allow you to select NIC to use as private, public, + or simply disabled +* Mgmt server will now reflect the status of user vms if the storage server that + hosts the vm's volumes is no longer accessible. (Bug 521) +* Routing and Console Proxy VM will now be HA-enabled by default (Bug 614) +* Console Proxy VM are now automatically managed by the Mgmt Server (Bug 110) +* Template Management from the console admin UI should be improved +* Too many to list... + +******************************************************************************** + VMOps Cloud Stack Version 0.2.6297 + Release Notes +******************************************************************************** + +================================= +WHAT'S NEW : +================================= + +* VMOps Server + - Introduction of VMOps Integration API. This API allows service providers + to provision users and to retrieve billing info and events. It is a simple + query language written on top of HTTP that simply returns results in XML + format. + - Improved VMOps Server installation process. + +* VMOps Multitenant Hypervisor + - Improved VMOps Multitenant Hypervisor installation process. + +================================= +KNOWN ISSUES : +================================= + +* PV drivers for Windows 2003 and Windows 2003 x86_64 (Incomplete Feature) +* Developer, Billing, and Provisioning API (Incomplete Feature) +* Mirroring of disk images on storage servers across pods (Incomplete Feature) +* HA Enabled VMs (Disabled Feature) +* Firewall integration API (Incomplete Feature) +* GUI panel for allowing administrators to set various system configurations - + i.e. zones, pods, ip addresses (Incomplete Feature) +* Support for multiple NIC cards on computing, routing, and storage servers + (Disabled Feature) +* Ability to deploy agent upgrade on VMOps server and have the upgrade + automatically propagated to storage, routing, and computing servers. + (Bug 386) +* Detailed IO stats for storage servers (Bug 94) +* Admin/User Dashboard (Bug 154 and 155) +* OpenSolaris kernel panic (Bug 413) +* Dynamically refresh server and vm instance status in GUI without a manual + refresh (Bug 389) +* Need transaction semantics across DB and agent. Without this, it is possible + to timeout db calls that first lock a record and relies on an agent response + before releasing that record. (Bug 408) +* All Server Roles (Mgmt, Computing, Routing, and Storage) require a functioning + eth0 NIC in order to install correctly. (Bug 470) +* Admin Console UI Templates Tab needs improvement. (Bug 469) +* Unable to create new admins through the admin console UI. (Bug 492) +* Unable to create new Service Offerings through the admin console UI (Bug 500) + +================================= +BUG FIXES : +================================= + +* Added a new XML (server-setup.xml) to configure initial VMOps Server data. + (Bug 430) +* Made installation of the router template easier (Bug 434) +* Deleting a user through the admin UI will now show a progress bar (Bug 428) +* You can no longer any drop down boxes in the search left panel of the console + UI (Bug 439) +* Configured dom0 and domR to no longer do any reverse DNS lookup (Bug 459) +* Fixed installer to handle multiple NIC (Bug 457) +* Missing SDL module in all templates (Bug 449) + +******************************************************************************** + VMOps Cloud Stack Version 0.1.6053 + Release Notes +******************************************************************************** + +================================= +WHAT'S NEW : +================================= + +* VMOps Server + - Complete web UI for both administrators and users to manage VMOps Cloud + Stack. + - Allows administrators to manage the creation of service offerings and set + its pricing along with pricing for network bandwidth, additional public + ips, and vm templates. + - Allows administrators to retrieve billing records and user usages. + - VM Sync - coordinates, manages, and maintains the life cycle of VMOps agents + running on attached computing, routing, and storage hosts. + - VM Template Management - allows administrators to manage and upload hosted + vm templates into VMOps Cloud Stack. + +* VMOps Multitenant Hypervisor + - Designed to allow for the complete isolation of CPU, memory, storage, and + network resources for each user. + • Hypervisor Attached Storage (HAS) – The storage solution that is + integrated within the hypervisor and does not rely on centralized SAN or NAS + to implement storage virtualization. It provides a high performance and + ultra-reliable block storage for virtual machine images + - Hypervisor Aware Network (HAN) – The network solution for VMOps Cloud Stack + that implements the necessary IP address translation and tunneling for the + guest OS running inside the virtual machine. It does not rely on VLAN to + implement any network virtualization and isolation. + +================================= +KNOWN ISSUES : +================================= + +* PV drivers for Windows 2003 and Windows 2003 x86_64 (Incomplete Feature) +* Developer, Billing, and Provisioning API (Incomplete Feature) +* Mirroring of disk images on storage servers across pods (Incomplete Feature) +* HA Enabled VMs (Disabled Feature) +* Firewall integration API (Incomplete Feature) +* GUI panel for allowing administrators to set various system configurations - + i.e. zones, pods, ip addresses (Incomplete Feature) +* Support for multiple NIC cards on computing, routing, and storage servers + (Disabled Feature) +* Ability to deploy agent upgrade on VMOps server and have the upgrade + automatically propagated to storage, routing, and computing servers. + (Bug 386) +* Detailed IO stats for storage servers (Bug 94) +* Admin/User Dashboard (Bug 154 and 155) +* OpenSolaris kernel panic (Bug 413) +* Dynamically refresh server and vm instance status in GUI without a manual + refresh (Bug 389) +* Need transaction semantics across DB and agent. Without this, it is possible + to timeout db calls that first lock a record and relies on an agent response + before releasing that record. (Bug 408) + +================================= +BUG FIXES : +================================= + +* N/A \ No newline at end of file diff --git a/build/replace.properties b/build/replace.properties new file mode 100644 index 00000000000..0b6b60c363f --- /dev/null +++ b/build/replace.properties @@ -0,0 +1,8 @@ +DBUSER=cloud +DBPW=cloud +MSLOG=vmops.log +APISERVERLOG=api.log +DBHOST=localhost +AGENTLOGDIR=logs +AGENTLOG=logs/agent.log +MSMNTDIR=/mnt diff --git a/client/.classpath b/client/.classpath new file mode 100644 index 00000000000..ae51d2b53e3 --- /dev/null +++ b/client/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/client/.project b/client/.project new file mode 100644 index 00000000000..f3ae78416f1 --- /dev/null +++ b/client/.project @@ -0,0 +1,17 @@ + + + vmopsClient + vmopsClient project + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/client/WEB-INF/web.xml b/client/WEB-INF/web.xml new file mode 100644 index 00000000000..21dd36e727d --- /dev/null +++ b/client/WEB-INF/web.xml @@ -0,0 +1,55 @@ + + + + + + DB Connection Pooling + jdbc/VmopsDB + javax.sql.DataSource + Container + + + + cloudStartupServlet + com.cloud.servlet.CloudStartupServlet + 4 + + + + apiServlet + com.cloud.api.ApiServlet + + + + consoleServlet + com.cloud.servlet.ConsoleProxyServlet + + + + apiServlet + /api/* + + + + consoleServlet + /console + + diff --git a/client/bindir/cloud-setup-management.in b/client/bindir/cloud-setup-management.in new file mode 100755 index 00000000000..c6cf6c98eae --- /dev/null +++ b/client/bindir/cloud-setup-management.in @@ -0,0 +1,288 @@ +#!/usr/bin/env python + +import sys, os, subprocess, errno, re + +# ---- This snippet of code adds the sources path and the waf configured PYTHONDIR to the Python path ---- +# ---- We do this so cloud_utils can be looked up in the following order: +# ---- 1) Sources directory +# ---- 2) waf configured PYTHONDIR +# ---- 3) System Python path +for pythonpath in ( + "@PYTHONDIR@", + os.path.join(os.path.dirname(__file__),os.path.pardir,os.path.pardir,"python","lib"), + ): + if os.path.isdir(pythonpath): sys.path.insert(0,pythonpath) +# ---- End snippet of code ---- +from cloud_utils import check_selinux, CheckFailed + +E_GENERIC= 1 +E_NOKVM = 2 +E_NODEFROUTE = 3 +E_DHCP = 4 +E_NOPERSISTENTNET = 5 +E_VIRTRECONFIGFAILED = 7 +E_FWRECONFIGFAILED = 8 +E_AGENTRECONFIGFAILED = 9 +E_AGENTFAILEDTOSTART = 10 +E_NOFQDN = 11 +E_OSUNSUPP = 11 +E_SUDORECONFIGFAILED = 6 + + +def stderr(msgfmt,*args): + msgfmt += "\n" + if args: sys.stderr.write(msgfmt%args) + else: sys.stderr.write(msgfmt) + sys.stderr.flush() + +def bail(errno=E_GENERIC,message=None,*args): + if message: stderr(message,*args) + stderr("CloudStack Management Server setup aborted") + sys.exit(errno) + + +#---------------- boilerplate for python 2.4 support + + +# CENTOS does not have this -- we have to put this here +try: + from subprocess import check_call + from subprocess import CalledProcessError +except ImportError: + def check_call(*popenargs, **kwargs): + import subprocess + retcode = subprocess.call(*popenargs, **kwargs) + cmd = kwargs.get("args") + if cmd is None: cmd = popenargs[0] + if retcode: raise CalledProcessError(retcode, cmd) + return retcode + + class CalledProcessError(Exception): + def __init__(self, returncode, cmd): + self.returncode = returncode ; self.cmd = cmd + def __str__(self): return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) + +# ------------ end boilerplate ------------------------- + + +def check_kvm(): return check_call(["kvm-ok"]) +def check_hostname(): return check_call(["hostname",'--fqdn']) + +class Command: + def __init__(self,name,parent=None): + self.__name = name + self.__parent = parent + def __getattr__(self,name): + if name == "_print": name = "print" + return Command(name,self) + def __call__(self,*args): + cmd = self.__get_recursive_name() + list(args) + #print " ",cmd + popen = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + m = popen.communicate() + ret = popen.wait() + if ret: + e = CalledProcessError(ret,cmd) + e.stdout,e.stderr = m + raise e + class CommandOutput: + def __init__(self,stdout,stderr): + self.stdout = stdout + self.stderr = stderr + return CommandOutput(*m) + def __lt__(self,other): + cmd = self.__get_recursive_name() + #print " ",cmd,"<",other + popen = subprocess.Popen(cmd,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + m = popen.communicate(other) + ret = popen.wait() + if ret: + e = CalledProcessError(ret,cmd) + e.stdout,e.stderr = m + raise e + class CommandOutput: + def __init__(self,stdout,stderr): + self.stdout = stdout + self.stderr = stderr + return CommandOutput(*m) + + def __get_recursive_name(self,sep=None): + m = self + l = [] + while m is not None: + l.append(m.__name) + m = m.__parent + l.reverse() + if sep: return sep.join(l) + else: return l + def __str__(self): + return ''%self.__get_recursive_name(sep=" ") + + def __repr__(self): return self.__str__() + +def checkselinux(): + try: + check_selinux() + except CheckFailed,e: + bail(E_SELINUXENABLED,"SELINUX is set to enforcing. Please set it to permissive in /etc/selinux/config. Then reboot or run setenforce permissive. Then run this program again.") + + +ip = Command("ip") +service = Command("service") +chkconfig = Command("chkconfig") +updatercd = Command("update-rc.d") +ufw = Command("ufw") +iptables = Command("iptables") +augtool = Command("augtool") +kvmok = Command("kvm-ok") +ifconfig = Command("ifconfig") +uuidgen = Command("uuidgen") + +Fedora = os.path.exists("/etc/fedora-release") +CentOS = os.path.exists("/etc/centos-release") or ( os.path.exists("/etc/redhat-release") and not os.path.exists("/etc/fedora-release") ) + +#--------------- procedure starts here ------------ + +stderr("Welcome to the CloudStack Management setup") +stderr("") + +try: + check_hostname() + stderr("The hostname of this machine is properly set up") +except CalledProcessError,e: + bail(E_NOFQDN,"This machine does not have an FQDN (fully-qualified domain name) for a hostname") + +if Fedora or CentOS: + checkselinux() + +try: + service("@PACKAGE@-management","status") + stderr("Stopping the CloudStack Management Server") + m = service("@PACKAGE@-management","stop") + print m.stdout + m.stderr + stderr("CloudStack Management Server stopped") +except CalledProcessError,e: + pass + +requiretty = augtool._print("/files/etc/sudoers/Defaults/requiretty").stdout.strip() +sudoeruser = augtool.match("/files/etc/sudoers/spec/user","@MSUSER@").stdout.strip() + +if requiretty: + sudoerstext = file("/etc/sudoers").read() + def restore(): + try: file("/etc/sudoers","w").write(sudoerstext) + except OSError,e: raise + + script = """rm %s +save"""%"/".join(requiretty.split("/")[:-1]) + + stderr("Executing the following reconfiguration script:\n%s",script) + + try: + returned = augtool < script + if "Saved 1 file" not in returned.stdout: + print returned.stdout + returned.stderr + restore() + bail(E_SUDORECONFIGFAILED,"sudoers reconfiguration failed.") + else: + stderr("sudoers reconfiguration complete") + except CalledProcessError,e: + restore() + print e.stdout + e.stderr + bail(E_SUDORECONFIGFAILED,"sudoers reconfiguration failed") + +if "@MSUSER@" not in sudoeruser: + sudoerstext = file("/etc/sudoers").read() + def restore(): + try: file("/etc/sudoers","w").write(sudoerstext) + except OSError,e: raise + + script = """ins spec after /files/etc/sudoers/spec[last()] +set /files/etc/sudoers/spec[last()]/user @MSUSER@ +set /files/etc/sudoers/spec[last()]/host_group/host ALL +set /files/etc/sudoers/spec[last()]/host_group/command ALL +set /files/etc/sudoers/spec[last()]/host_group/command/tag NOPASSWD +save""" + + stderr("Executing the following reconfiguration script:\n%s",script) + + try: + returned = augtool < script + if "Saved 1 file" not in returned.stdout: + print returned.stdout + returned.stderr + restore() + bail(E_SUDORECONFIGFAILED,"sudoers reconfiguration failed.") + else: + stderr("sudoers reconfiguration complete") + except CalledProcessError,e: + restore() + print e.stdout + e.stderr + bail(E_SUDORECONFIGFAILED,"sudoers reconfiguration failed") + +ports = "80 8080 8096 8250 9090".split() +if Fedora or CentOS: + try: + o = chkconfig("--list","iptables") + iptableschkconfig = True + except CalledProcessError,e: + stderr("No need to set up iptables as the service is not registered in the SysV init system") + iptableschkconfig = False + if iptableschkconfig is True: + try: + o = chkconfig("--list","iptables") + if ":on" in o.stdout and os.path.exists("/etc/sysconfig/iptables"): + stderr("Setting up firewall rules to permit traffic to CloudStack services") + service.iptables.start() ; print o.stdout + o.stderr + for p in ports: iptables("-I","INPUT","1","-p","tcp","--dport",p,"-j","ACCEPT") + o = service.iptables.save() ; print o.stdout + o.stderr + else: + stderr("No need to set up iptables as the service is unconfigured or not set to start up at boot") + except CalledProcessError,e: + print e.stdout+e.stderr + bail(E_FWRECONFIGFAILED,"Firewall rules could not be set") +else: + stderr("Setting up firewall rules to permit traffic to CloudStack services") + try: + for p in ports: ufw.allow(p) + stderr("Rules set") + except CalledProcessError,e: + print e.stdout+e.stderr + bail(E_FWRECONFIGFAILED,"Firewall rules could not be set") + + stderr("We are going to enable ufw now. This may disrupt network connectivity and service availability. See the ufw documentation for information on how to manage ufw firewall policies.") + try: + o = ufw.enable < "y\n" ; print o.stdout + o.stderr + except CalledProcessError,e: + print e.stdout+e.stderr + bail(E_FWRECONFIGFAILED,"Firewall could not be enabled") + +stderr("") +stderr("CloudStack Management Server setup completed successfully") + +try: + if Fedora or CentOS: chkconfig("tomcat6","off") + else: updatercd("tomcat6","disable") + service("tomcat6","status") + stderr("Stopping Tomcat") + m = service("tomcat6","stop") + print m.stdout + m.stderr + stderr("Tomcat stopped") +except CalledProcessError,e: + pass + +try: + if Fedora or CentOS: + chkconfig("--level","35","network","on") +except CalledProcessError,e: + pass + +stderr("Starting the CloudStack Management Server") +try: + m = service("@PACKAGE@-management","start") + print m.stdout + m.stderr +except CalledProcessError,e: + print e.stdout + e.stderr + bail(E_AGENTFAILEDTOSTART,"@PACKAGE@-management failed to start") + + +# FIXMES: 1) nullify networkmanager on ubuntu (asking the user first) and enable the networking service permanently diff --git a/client/bindir/cloud-update-xenserver-licenses.in b/client/bindir/cloud-update-xenserver-licenses.in new file mode 100755 index 00000000000..6249f7805fe --- /dev/null +++ b/client/bindir/cloud-update-xenserver-licenses.in @@ -0,0 +1,165 @@ +#!/usr/bin/python -W ignore::DeprecationWarning +# -*- coding: utf-8 -*- + +import os +import sys +import glob +from random import choice +import string +from optparse import OptionParser +import MySQLdb +import paramiko +from threading import Thread + +# ---- This snippet of code adds the sources path and the waf configured PYTHONDIR to the Python path ---- +# ---- We do this so cloud_utils can be looked up in the following order: +# ---- 1) Sources directory +# ---- 2) waf configured PYTHONDIR +# ---- 3) System Python path +for pythonpath in ( + "@PYTHONDIR@", + os.path.join(os.path.dirname(__file__),os.path.pardir,os.path.pardir,"python","lib"), + ): + if os.path.isdir(pythonpath): sys.path.insert(0,pythonpath) +# ---- End snippet of code ---- +from cloud_utils import check_call, CalledProcessError, read_properties + +cfg = "@MSCONF@/db.properties" + +#---------------------- option parsing and command line checks ------------------------ + + +usage = """%prog <-a | host names / IP addresses...> + +This command deploys the license file specified in the command line into a specific XenServer host or all XenServer hosts known to the management server.""" + +parser = OptionParser(usage=usage) +parser.add_option("-a", "--all", action="store_true", dest="all", default=False, + help="deploy to all known hosts rather that a single host") + +#------------------ functions -------------------- + +def e(msg): parser.error(msg) + +def getknownhosts(host,username,password): + conn = MySQLdb.connect(host=host,user=username,passwd=password) + cur = conn.cursor() + cur.execute("SELECT h.private_ip_address,d.value FROM cloud.host h inner join cloud.host_details d on (h.id = d.host_id) where d.name = 'username' and setup = 1") + usernames = dict(cur.fetchall()) + cur.execute("SELECT h.private_ip_address,d.value FROM cloud.host h inner join cloud.host_details d on (h.id = d.host_id) where d.name = 'password' and setup = 1") + passwords = dict(cur.fetchall()) + creds = dict( [ [x,(usernames[x],passwords[x])] for x in usernames.keys() ] ) + cur.close() + conn.close() + return creds + +def splitlast(string,splitter): + splitted = string.split(splitter) + first,last = splitter.join(splitted[:-1]),splitted[-1] + return first,last + +def parseuserpwfromhosts(hosts): + creds = {} + for host in hosts: + user = "root" + password = "" + if "@" in host: + user,host = splitlast(host,"@") + if ":" in user: + user,password = splitlast(user,":") + creds[host] = (user,password) + return creds + +class XenServerConfigurator(Thread): + + def __init__(self,host,user,password,keyfiledata): + Thread.__init__(self) + self.host = host + self.user = user + self.password = password + self.keyfiledata = keyfiledata + self.retval = None # means all's good + self.stdout = "" + self.stderr = "" + self.state = 'initialized' + + def run(self): + try: + self.state = 'running' + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect(self.host,username=self.user,password=self.password) + sftp = c.open_sftp() + sftp.chdir("/tmp") + f = sftp.open("xen-license","w") + f.write(self.keyfiledata) + f.close() + sftp.close() + stdin,stdout,stderr = c.exec_command("xe host-license-add license-file=/tmp/xen-license") + c.exec_command("false") + self.stdout = stdout.read(-1) + self.stderr = stderr.read(-1) + self.retval = stdin.channel.recv_exit_status() + c.close() + if self.retval != 0: self.state = 'failed' + else: self.state = 'finished' + + except Exception,e: + self.state = 'failed' + self.retval = e + #raise + + def __str__(self): + if self.state == 'failed': + return "<%s XenServerConfigurator on %s@%s: %s>"%(self.state,self.user,self.host,str(self.retval)) + else: + return "<%s XenServerConfigurator on %s@%s>"%(self.state,self.user,self.host) + +#------------- actual code -------------------- + +(options, args) = parser.parse_args() + +try: + licensefile,args = args[0],args[1:] +except IndexError: e("The first argument must be the license file to use") + +if options.all: + if len(args) != 0: e("IP addresses cannot be specified if -a is specified") + config = cloud_utils.read_properties(cfg) + creds = getknownhosts(config["db.cloud.host"],config["db.cloud.username"],config["db.cloud.password"]) + hosts = creds.keys() +else: + if not args: e("You must specify at least one IP address, or -a") + hosts = args + creds = parseuserpwfromhosts(hosts) + +try: + keyfiledata = file(licensefile).read(-1) +except OSError,e: + sys.stderr.write("The file %s cannot be opened"%licensefile) + sys.exit(1) + +configurators = [] +for host,(user,password) in creds.items(): + configurators.append ( XenServerConfigurator(host,user,password,keyfiledata ) ) + + +for c in configurators: c.start() + +for c in configurators: + print c.host + "...", + c.join() + if c.state == 'failed': + if c.retval: + msg = "failed with return code %s: %s%s"%(c.retval,c.stdout,c.stderr) + msg = msg.strip() + print msg + else: print "failed: %s"%c.retval + else: + print "done" + +successes = len( [ a for a in configurators if not a.state == 'failed' ] ) +failures = len( [ a for a in configurators if a.state == 'failed' ] ) + +print "%3s successes"%successes +print "%3s failures"%failures diff --git a/client/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-management.in b/client/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-management.in new file mode 100755 index 00000000000..d61cb7b8088 --- /dev/null +++ b/client/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-management.in @@ -0,0 +1,41 @@ +#!/bin/bash +# +# @PACKAGE@-management This shell script takes care of starting and stopping Tomcat +# +# chkconfig: - 80 20 +# +### BEGIN INIT INFO +# Provides: tomcat6 +# Required-Start: $network $syslog +# Required-Stop: $network $syslog +# Default-Start: +# Default-Stop: +# Description: Release implementation for Servlet 2.5 and JSP 2.1 +# Short-Description: start and stop tomcat +### END INIT INFO +# +# - originally written by Henri Gomez, Keith Irwin, and Nicolas Mailhot +# - heavily rewritten by Deepak Bhole and Jason Corley +# + +if [ -r /etc/rc.d/init.d/functions ]; then + . /etc/rc.d/init.d/functions +fi +if [ -r /lib/lsb/init-functions ]; then + . /lib/lsb/init-functions +fi + + +NAME="$(basename $0)" + +# See how we were called. +case "$1" in + status) + status ${NAME} + RETVAL=$? + ;; + *) + . /etc/rc.d/init.d/tomcat6 +esac + +exit $RETVAL diff --git a/client/distro/centos/SYSCONFDIR/sysconfig/cloud-management.in b/client/distro/centos/SYSCONFDIR/sysconfig/cloud-management.in new file mode 100644 index 00000000000..2d4b22f4bff --- /dev/null +++ b/client/distro/centos/SYSCONFDIR/sysconfig/cloud-management.in @@ -0,0 +1,6 @@ +# This file is loaded in /etc/init.d/vmopsmanagement +# ATM we only do two things here: + +dummy=1 ; export TOMCAT_CFG=@MSCONF@/tomcat6.conf ; . @MSCONF@/tomcat6.conf +#-------------------------- + diff --git a/client/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-management.in b/client/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-management.in new file mode 100755 index 00000000000..d61cb7b8088 --- /dev/null +++ b/client/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-management.in @@ -0,0 +1,41 @@ +#!/bin/bash +# +# @PACKAGE@-management This shell script takes care of starting and stopping Tomcat +# +# chkconfig: - 80 20 +# +### BEGIN INIT INFO +# Provides: tomcat6 +# Required-Start: $network $syslog +# Required-Stop: $network $syslog +# Default-Start: +# Default-Stop: +# Description: Release implementation for Servlet 2.5 and JSP 2.1 +# Short-Description: start and stop tomcat +### END INIT INFO +# +# - originally written by Henri Gomez, Keith Irwin, and Nicolas Mailhot +# - heavily rewritten by Deepak Bhole and Jason Corley +# + +if [ -r /etc/rc.d/init.d/functions ]; then + . /etc/rc.d/init.d/functions +fi +if [ -r /lib/lsb/init-functions ]; then + . /lib/lsb/init-functions +fi + + +NAME="$(basename $0)" + +# See how we were called. +case "$1" in + status) + status ${NAME} + RETVAL=$? + ;; + *) + . /etc/rc.d/init.d/tomcat6 +esac + +exit $RETVAL diff --git a/client/distro/fedora/SYSCONFDIR/sysconfig/cloud-management.in b/client/distro/fedora/SYSCONFDIR/sysconfig/cloud-management.in new file mode 100644 index 00000000000..2d4b22f4bff --- /dev/null +++ b/client/distro/fedora/SYSCONFDIR/sysconfig/cloud-management.in @@ -0,0 +1,6 @@ +# This file is loaded in /etc/init.d/vmopsmanagement +# ATM we only do two things here: + +dummy=1 ; export TOMCAT_CFG=@MSCONF@/tomcat6.conf ; . @MSCONF@/tomcat6.conf +#-------------------------- + diff --git a/client/distro/ubuntu/SYSCONFDIR/init.d/cloud-management.in b/client/distro/ubuntu/SYSCONFDIR/init.d/cloud-management.in new file mode 100755 index 00000000000..4d599d86d6a --- /dev/null +++ b/client/distro/ubuntu/SYSCONFDIR/init.d/cloud-management.in @@ -0,0 +1,229 @@ +#!/bin/sh +# +# /etc/init.d/tomcat6 -- startup script for the Tomcat 6 servlet engine +# +# Written by Miquel van Smoorenburg . +# Modified for Debian GNU/Linux by Ian Murdock . +# Modified for Tomcat by Stefan Gybas . +# Modified for Tomcat6 by Thierry Carrez . +# Modified for VMOps by Manuel Amador (Rudd-O) +# +### BEGIN INIT INFO +# Provides: tomcat-vmops +# Required-Start: $local_fs $remote_fs $network +# Required-Stop: $local_fs $remote_fs $network +# Should-Start: $named +# Should-Stop: $named +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start Tomcat (CloudStack). +# Description: Start the Tomcat servlet engine that runs the CloudStack Management Server. +### END INIT INFO + +PATH=/bin:/usr/bin:/sbin:/usr/sbin +NAME=cloud-management +DESC="CloudStack-specific Tomcat servlet engine" +DAEMON=/usr/bin/jsvc +CATALINA_HOME=@MSENVIRON@ +DEFAULT=@MSCONF@/tomcat6.conf +JVM_TMP=/tmp/$NAME-temp + +if [ `id -u` -ne 0 ]; then + echo "You need root privileges to run this script" + exit 1 +fi + +# Make sure tomcat is started with system locale +if [ -r /etc/default/locale ]; then + . /etc/default/locale + export LANG +fi + +. /lib/lsb/init-functions +. /etc/default/rcS + + +# The following variables can be overwritten in $DEFAULT + +# Run Tomcat 6 as this user ID +TOMCAT6_USER=tomcat6 + +# The first existing directory is used for JAVA_HOME (if JAVA_HOME is not +# defined in $DEFAULT) +JDK_DIRS="/usr/lib/jvm/java-6-openjdk /usr/lib/jvm/java-6-sun /usr/lib/jvm/java-1.5.0-sun /usr/lib/j2sdk1.5-sun /usr/lib/j2sdk1.5-ibm" + +# Look for the right JVM to use +for jdir in $JDK_DIRS; do + if [ -r "$jdir/bin/java" -a -z "${JAVA_HOME}" ]; then + JAVA_HOME="$jdir" + fi +done +export JAVA_HOME + +# Directory for per-instance configuration files and webapps +CATALINA_BASE=@MSENVIRON@ + +# Use the Java security manager? (yes/no) +TOMCAT6_SECURITY=no + +# Default Java options +# Set java.awt.headless=true if JAVA_OPTS is not set so the +# Xalan XSL transformer can work without X11 display on JDK 1.4+ +# It also looks like the default heap size of 64M is not enough for most cases +# so the maximum heap size is set to 128M +if [ -z "$JAVA_OPTS" ]; then + JAVA_OPTS="-Djava.awt.headless=true -Xmx128M" +fi + +# End of variables that can be overwritten in $DEFAULT + +# overwrite settings from default file +if [ -f "$DEFAULT" ]; then + . "$DEFAULT" +fi + +if [ ! -f "$CATALINA_HOME/bin/bootstrap.jar" ]; then + log_failure_msg "$NAME is not installed" + exit 1 +fi + +[ -f "$DAEMON" ] || exit 0 + +POLICY_CACHE="$CATALINA_BASE/work/catalina.policy" + +JAVA_OPTS="$JAVA_OPTS -Djava.endorsed.dirs=$CATALINA_HOME/endorsed -Dcatalina.base=$CATALINA_BASE -Dcatalina.home=$CATALINA_HOME -Djava.io.tmpdir=$JVM_TMP" + +# Set the JSP compiler if set in the tomcat6.default file +if [ -n "$JSP_COMPILER" ]; then + JAVA_OPTS="$JAVA_OPTS -Dbuild.compiler=$JSP_COMPILER" +fi + +if [ "$TOMCAT6_SECURITY" = "yes" ]; then + JAVA_OPTS="$JAVA_OPTS -Djava.security.manager -Djava.security.policy=$POLICY_CACHE" +fi + +# Set juli LogManager if logging.properties is provided +if [ -r "$CATALINA_BASE"/conf/logging.properties ]; then + JAVA_OPTS="$JAVA_OPTS "-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager" "-Djava.util.logging.config.file="$CATALINA_BASE/conf/logging.properties" +fi + +# Define other required variables +CATALINA_PID="@PIDDIR@/$NAME.pid" +BOOTSTRAP_CLASS=org.apache.catalina.startup.Bootstrap +JSVC_CLASSPATH="/usr/share/java/commons-daemon.jar:$CATALINA_HOME/bin/bootstrap.jar" +JSVC_CLASSPATH=$CLASSPATH:$JSVC_CLASSPATH + +# Look for Java Secure Sockets Extension (JSSE) JARs +if [ -z "${JSSE_HOME}" -a -r "${JAVA_HOME}/jre/lib/jsse.jar" ]; then + JSSE_HOME="${JAVA_HOME}/jre/" +fi +export JSSE_HOME + +case "$1" in + start) + if [ -z "$JAVA_HOME" ]; then + log_failure_msg "no JDK found - please set JAVA_HOME" + exit 1 + fi + + if [ ! -d "$CATALINA_BASE/conf" ]; then + log_failure_msg "invalid CATALINA_BASE: $CATALINA_BASE" + exit 1 + fi + + log_daemon_msg "Starting $DESC" "$NAME" + if start-stop-daemon --test --start --pidfile "$CATALINA_PID" \ + --user $TOMCAT6_USER --startas "$JAVA_HOME/bin/java" \ + >/dev/null; then + + # Regenerate POLICY_CACHE file + umask 022 + echo "// AUTO-GENERATED FILE from /etc/tomcat6/policy.d/" \ + > "$POLICY_CACHE" + echo "" >> "$POLICY_CACHE" + if ls $CATALINA_BASE/conf/policy.d/*.policy > /dev/null 2>&1 ; then + cat $CATALINA_BASE/conf/policy.d/*.policy \ + >> "$POLICY_CACHE" + fi + + # Remove / recreate JVM_TMP directory + rm -rf "$JVM_TMP" + mkdir "$JVM_TMP" || { + log_failure_msg "could not create JVM temporary directory" + exit 1 + } + chown $TOMCAT6_USER "$JVM_TMP" + cd "$JVM_TMP" + + + # fix storage issues on nfs mounts + umask 000 + $DAEMON -user "$TOMCAT6_USER" -cp "$JSVC_CLASSPATH" \ + -outfile SYSLOG -errfile SYSLOG \ + -pidfile "$CATALINA_PID" $JAVA_OPTS "$BOOTSTRAP_CLASS" + + sleep 5 + if start-stop-daemon --test --start --pidfile "$CATALINA_PID" \ + --user $TOMCAT6_USER --startas "$JAVA_HOME/bin/java" \ + >/dev/null; then + log_end_msg 1 + else + log_end_msg 0 + fi + else + log_progress_msg "(already running)" + log_end_msg 0 + fi + ;; + stop) + log_daemon_msg "Stopping $DESC" "$NAME" + if start-stop-daemon --test --start --pidfile "$CATALINA_PID" \ + --user "$TOMCAT6_USER" --startas "$JAVA_HOME/bin/java" \ + >/dev/null; then + log_progress_msg "(not running)" + else + $DAEMON -cp "$JSVC_CLASSPATH" -pidfile "$CATALINA_PID" \ + -stop "$BOOTSTRAP_CLASS" + fi + rm -rf "$JVM_TMP" + log_end_msg 0 + ;; + status) + if start-stop-daemon --test --start --pidfile "$CATALINA_PID" \ + --user $TOMCAT6_USER --startas "$JAVA_HOME/bin/java" \ + >/dev/null; then + + if [ -f "$CATALINA_PID" ]; then + log_success_msg "$DESC is not running, but pid file exists." + exit 1 + else + log_success_msg "$DESC is not running." + exit 3 + fi + else + log_success_msg "$DESC is running with pid `cat $CATALINA_PID`" + fi + ;; + restart|force-reload) + if start-stop-daemon --test --stop --pidfile "$CATALINA_PID" \ + --user $TOMCAT6_USER --startas "$JAVA_HOME/bin/java" \ + >/dev/null; then + $0 stop + sleep 1 + fi + $0 start + ;; + try-restart) + if start-stop-daemon --test --start --pidfile "$CATALINA_PID" \ + --user $TOMCAT6_USER --startas "$JAVA_HOME/bin/java" \ + >/dev/null; then + $0 start + fi + ;; + *) + log_success_msg "Usage: $0 {start|stop|restart|try-restart|force-reload|status}" + exit 1 + ;; +esac + +exit 0 diff --git a/client/tomcatconf/catalina.policy.in b/client/tomcatconf/catalina.policy.in new file mode 100644 index 00000000000..4bbfbf29058 --- /dev/null +++ b/client/tomcatconf/catalina.policy.in @@ -0,0 +1,180 @@ +// 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. + +// ============================================================================ +// catalina.corepolicy - Security Policy Permissions for Tomcat 6 +// +// This file contains a default set of security policies to be enforced (by the +// JVM) when Catalina is executed with the "-security" option. In addition +// to the permissions granted here, the following additional permissions are +// granted to the codebase specific to each web application: +// +// * Read access to the document root directory +// +// $Id: catalina.policy 899134 2010-01-14 09:44:28Z rjung $ +// ============================================================================ + + +// ========== SYSTEM CODE PERMISSIONS ========================================= + + +// These permissions apply to javac +grant codeBase "file:${java.home}/lib/-" { + permission java.security.AllPermission; +}; + +// These permissions apply to all shared system extensions +grant codeBase "file:${java.home}/jre/lib/ext/-" { + permission java.security.AllPermission; +}; + +// These permissions apply to javac when ${java.home] points at $JAVA_HOME/jre +grant codeBase "file:${java.home}/../lib/-" { + permission java.security.AllPermission; +}; + +// These permissions apply to all shared system extensions when +// ${java.home} points at $JAVA_HOME/jre +grant codeBase "file:${java.home}/lib/ext/-" { + permission java.security.AllPermission; +}; + + +// ========== CATALINA CODE PERMISSIONS ======================================= + + +// These permissions apply to the daemon code +grant codeBase "file:${catalina.home}/bin/commons-daemon.jar" { + permission java.security.AllPermission; +}; + +// These permissions apply to the logging API +grant codeBase "file:${catalina.home}/bin/tomcat-juli.jar" { + permission java.util.PropertyPermission "java.util.logging.config.class", "read"; + permission java.util.PropertyPermission "java.util.logging.config.file", "read"; + permission java.io.FilePermission "${java.home}${file.separator}lib${file.separator}logging.properties", "read"; + permission java.lang.RuntimePermission "shutdownHooks"; + permission java.io.FilePermission "${catalina.base}${file.separator}conf${file.separator}logging.properties", "read"; + permission java.util.PropertyPermission "catalina.base", "read"; + permission java.util.logging.LoggingPermission "control"; + permission java.io.FilePermission "${catalina.base}${file.separator}logs", "read, write"; + permission java.io.FilePermission "${catalina.base}${file.separator}logs${file.separator}*", "read, write"; + permission java.lang.RuntimePermission "getClassLoader"; + permission java.lang.RuntimePermission "setContextClassLoader"; + // To enable per context logging configuration, permit read access to the appropriate file. + // Be sure that the logging configuration is secure before enabling such access + // eg for the examples web application: + // permission java.io.FilePermission "${catalina.base}${file.separator}webapps${file.separator}examples${file.separator}WEB-INF${file.separator}classes${file.separator}logging.properties", "read"; +}; + +// These permissions apply to the server startup code +grant codeBase "file:${catalina.home}/bin/bootstrap.jar" { + permission java.security.AllPermission; +}; + +// These permissions apply to the servlet API classes +// and those that are shared across all class loaders +// located in the "lib" directory +grant codeBase "file:${catalina.home}/lib/-" { + permission java.security.AllPermission; +}; + + +// ========== WEB APPLICATION PERMISSIONS ===================================== + + +// These permissions are granted by default to all web applications +// In addition, a web application will be given a read FilePermission +// and JndiPermission for all files and directories in its document root. +grant { + // Required for JNDI lookup of named JDBC DataSource's and + // javamail named MimePart DataSource used to send mail + permission java.util.PropertyPermission "java.home", "read"; + permission java.util.PropertyPermission "java.naming.*", "read"; + permission java.util.PropertyPermission "javax.sql.*", "read"; + + // OS Specific properties to allow read access + permission java.util.PropertyPermission "os.name", "read"; + permission java.util.PropertyPermission "os.version", "read"; + permission java.util.PropertyPermission "os.arch", "read"; + permission java.util.PropertyPermission "file.separator", "read"; + permission java.util.PropertyPermission "path.separator", "read"; + permission java.util.PropertyPermission "line.separator", "read"; + + // JVM properties to allow read access + permission java.util.PropertyPermission "java.version", "read"; + permission java.util.PropertyPermission "java.vendor", "read"; + permission java.util.PropertyPermission "java.vendor.url", "read"; + permission java.util.PropertyPermission "java.class.version", "read"; + permission java.util.PropertyPermission "java.specification.version", "read"; + permission java.util.PropertyPermission "java.specification.vendor", "read"; + permission java.util.PropertyPermission "java.specification.name", "read"; + + permission java.util.PropertyPermission "java.vm.specification.version", "read"; + permission java.util.PropertyPermission "java.vm.specification.vendor", "read"; + permission java.util.PropertyPermission "java.vm.specification.name", "read"; + permission java.util.PropertyPermission "java.vm.version", "read"; + permission java.util.PropertyPermission "java.vm.vendor", "read"; + permission java.util.PropertyPermission "java.vm.name", "read"; + + // Required for OpenJMX + permission java.lang.RuntimePermission "getAttribute"; + + // Allow read of JAXP compliant XML parser debug + permission java.util.PropertyPermission "jaxp.debug", "read"; + + // Precompiled JSPs need access to these packages. + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.jasper.el"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.jasper.runtime"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.jasper.runtime.*"; + + // Precompiled JSPs need access to these system properties. + permission java.util.PropertyPermission "org.apache.jasper.runtime.BodyContentImpl.LIMIT_BUFFER", "read"; + permission java.util.PropertyPermission "org.apache.el.parser.COERCE_TO_ZERO", "read"; +}; + + +// You can assign additional permissions to particular web applications by +// adding additional "grant" entries here, based on the code base for that +// application, /WEB-INF/classes/, or /WEB-INF/lib/ jar files. +// +// Different permissions can be granted to JSP pages, classes loaded from +// the /WEB-INF/classes/ directory, all jar files in the /WEB-INF/lib/ +// directory, or even to individual jar files in the /WEB-INF/lib/ directory. +// +// For instance, assume that the standard "examples" application +// included a JDBC driver that needed to establish a network connection to the +// corresponding database and used the scrape taglib to get the weather from +// the NOAA web server. You might create a "grant" entries like this: +// +// The permissions granted to the context root directory apply to JSP pages. +// grant codeBase "file:${catalina.home}/webapps/examples/-" { +// permission java.net.SocketPermission "dbhost.mycompany.com:5432", "connect"; +// permission java.net.SocketPermission "*.noaa.gov:80", "connect"; +// }; +// +// The permissions granted to the context WEB-INF/classes directory +// grant codeBase "file:${catalina.home}/webapps/examples/WEB-INF/classes/-" { +// }; +// +// The permission granted to your JDBC driver +// grant codeBase "jar:file:${catalina.home}/webapps/examples/WEB-INF/lib/driver.jar!/-" { +// permission java.net.SocketPermission "dbhost.mycompany.com:5432", "connect"; +// }; +// The permission granted to the scrape taglib +// grant codeBase "jar:file:${catalina.home}/webapps/examples/WEB-INF/lib/scrape.jar!/-" { +// permission java.net.SocketPermission "*.noaa.gov:80", "connect"; +// }; + diff --git a/client/tomcatconf/catalina.properties.in b/client/tomcatconf/catalina.properties.in new file mode 100644 index 00000000000..dc2db354920 --- /dev/null +++ b/client/tomcatconf/catalina.properties.in @@ -0,0 +1,81 @@ +# 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. + +# +# List of comma-separated packages that start with or equal this string +# will cause a security exception to be thrown when +# passed to checkPackageAccess unless the +# corresponding RuntimePermission ("accessClassInPackage."+package) has +# been granted. +package.access=sun.,org.apache.catalina.,org.apache.coyote.,org.apache.tomcat.,org.apache.jasper.,sun.beans. +# +# List of comma-separated packages that start with or equal this string +# will cause a security exception to be thrown when +# passed to checkPackageDefinition unless the +# corresponding RuntimePermission ("defineClassInPackage."+package) has +# been granted. +# +# by default, no packages are restricted for definition, and none of +# the class loaders supplied with the JDK call checkPackageDefinition. +# +package.definition=sun.,java.,org.apache.catalina.,org.apache.coyote.,org.apache.tomcat.,org.apache.jasper. + +# +# +# List of comma-separated paths defining the contents of the "common" +# classloader. Prefixes should be used to define what is the repository type. +# Path may be relative to the CATALINA_HOME or CATALINA_BASE path or absolute. +# If left as blank,the JVM system loader will be used as Catalina's "common" +# loader. +# Examples: +# "foo": Add this folder as a class repository +# "foo/*.jar": Add all the JARs of the specified folder as class +# repositories +# "foo/bar.jar": Add bar.jar as a class repository +common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar + +# +# List of comma-separated paths defining the contents of the "server" +# classloader. Prefixes should be used to define what is the repository type. +# Path may be relative to the CATALINA_HOME or CATALINA_BASE path or absolute. +# If left as blank, the "common" loader will be used as Catalina's "server" +# loader. +# Examples: +# "foo": Add this folder as a class repository +# "foo/*.jar": Add all the JARs of the specified folder as class +# repositories +# "foo/bar.jar": Add bar.jar as a class repository +server.loader= + +# +# List of comma-separated paths defining the contents of the "shared" +# classloader. Prefixes should be used to define what is the repository type. +# Path may be relative to the CATALINA_BASE path or absolute. If left as blank, +# the "common" loader will be used as Catalina's "shared" loader. +# Examples: +# "foo": Add this folder as a class repository +# "foo/*.jar": Add all the JARs of the specified folder as class +# repositories +# "foo/bar.jar": Add bar.jar as a class repository +# Please note that for single jars, e.g. bar.jar, you need the URL form +# starting with file:. +shared.loader= + +# +# String cache configuration. +tomcat.util.buf.StringCache.byte.enabled=true +#tomcat.util.buf.StringCache.char.enabled=true +#tomcat.util.buf.StringCache.trainThreshold=500000 +#tomcat.util.buf.StringCache.cacheSize=5000 diff --git a/client/tomcatconf/classpath.conf.in b/client/tomcatconf/classpath.conf.in new file mode 100644 index 00000000000..e14d1470382 --- /dev/null +++ b/client/tomcatconf/classpath.conf.in @@ -0,0 +1,22 @@ +#!/bin/bash + +SYSTEMJARS="@SYSTEMJARS@" +SCP=$(build-classpath $SYSTEMJARS 2>/dev/null) ; if [ $? != 0 ] ; then export SCP="@SYSTEMCLASSPATH@" ; fi +MCP="@MSCLASSPATH@" +DCP="@DEPSCLASSPATH@" +CLASSPATH=$SCP:$DCP:$MCP:@MSCONF@ +for jarfile in "@PREMIUMJAVADIR@"/* ; do + if [ ! -e "$jarfile" ] ; then continue ; fi + CLASSPATH=$jarfile:$CLASSPATH +done +for plugin in "@PLUGINJAVADIR@"/* ; do + if [ ! -e "$plugin" ] ; then continue ; fi + CLASSPATH=$plugin:$CLASSPATH +done +for vendorconf in "@MSCONF@"/vendor/* ; do + if [ ! -d "$vendorconf" ] ; then continue ; fi + CLASSPATH=$vendorconf:$CLASSPATH +done +export CLASSPATH +PATH=/sbin:/usr/sbin:$PATH +export PATH diff --git a/client/tomcatconf/commands.properties.in b/client/tomcatconf/commands.properties.in new file mode 100644 index 00000000000..fb62d18615e --- /dev/null +++ b/client/tomcatconf/commands.properties.in @@ -0,0 +1,210 @@ +### bitmap of permissions at the end of each classname, 1 = ADMIN, 2 = DOMAIN_ADMIN, 4 = READ_ONLY_ADMIN, 8 = USER +### Please standardize naming conventions to camel-case (even for acronyms). + +### Account commands +updateAccount=com.cloud.api.commands.UpdateAccountCmd;3 +disableAccount=com.cloud.api.commands.DisableAccountCmd;3 +enableAccount=com.cloud.api.commands.EnableAccountCmd;3 +lockAccount=com.cloud.api.commands.LockAccountCmd;3 +listAccounts=com.cloud.api.commands.ListAccountsCmd;15 + +#### User commands +createUser=com.cloud.api.commands.CreateUserCmd;1 +updateUser=com.cloud.api.commands.UpdateUserCmd;1 +deleteUser=com.cloud.api.commands.DeleteUserCmd;1 +listUsers=com.cloud.api.commands.ListUsersCmd;7 +lockUser=com.cloud.api.commands.LockUserCmd;3 +disableUser=com.cloud.api.commands.DisableUserCmd;3 +enableUser=com.cloud.api.commands.EnableUserCmd;3 + + +#### Domain commands +createDomain=com.cloud.api.commands.CreateDomainCmd;3 +updateDomain=com.cloud.api.commands.UpdateDomainCmd;3 +deleteDomain=com.cloud.api.commands.DeleteDomainCmd;3 +listDomains=com.cloud.api.commands.ListDomainsCmd;7 +listDomainChildren=com.cloud.api.commands.ListDomainChildrenCmd;7 + +####Cloud Identifier commands +getCloudIdentifier=com.cloud.api.commands.GetCloudIdentifierCmd;15 + +#### Limit commands +updateResourceLimit=com.cloud.api.commands.UpdateResourceLimitCmd;3 +listResourceLimits=com.cloud.api.commands.ListResourceLimitsCmd;15 + +#### VM commands +deployVirtualMachine=com.cloud.api.commands.DeployVMCmd;11 +destroyVirtualMachine=com.cloud.api.commands.DestroyVMCmd;15 +rebootVirtualMachine=com.cloud.api.commands.RebootVMCmd;15 +startVirtualMachine=com.cloud.api.commands.StartVMCmd;15 +stopVirtualMachine=com.cloud.api.commands.StopVMCmd;15 +resetPasswordForVirtualMachine=com.cloud.api.commands.ResetVMPasswordCmd;15 +changeServiceForVirtualMachine=com.cloud.api.commands.UpgradeVMCmd;15 +updateVirtualMachine=com.cloud.api.commands.UpdateVMCmd;15 +recoverVirtualMachine=com.cloud.api.commands.RecoverVMCmd;3 +listVirtualMachines=com.cloud.api.commands.ListVMsCmd;15 + +#### snapshot commands +createSnapshot=com.cloud.api.commands.CreateSnapshotCmd;15 +listSnapshots=com.cloud.api.commands.ListSnapshotsCmd;15 +deleteSnapshot=com.cloud.api.commands.DeleteSnapshotCmd;15 +createSnapshotPolicy=com.cloud.api.commands.CreateSnapshotPolicyCmd;15 +deleteSnapshotPolicies=com.cloud.api.commands.DeleteSnapshotPoliciesCmd;15 +listSnapshotPolicies=com.cloud.api.commands.ListSnapshotPoliciesCmd;15 + +#### template commands +createTemplate=com.cloud.api.commands.CreateTemplateCmd;15 +registerTemplate=com.cloud.api.commands.RegisterTemplateCmd;15 +updateTemplate=com.cloud.api.commands.UpdateTemplateCmd;15 +copyTemplate=com.cloud.api.commands.CopyTemplateCmd;15 +deleteTemplate=com.cloud.api.commands.DeleteTemplateCmd;15 +listTemplates=com.cloud.api.commands.ListTemplatesCmd;15 +updateTemplatePermissions=com.cloud.api.commands.UpdateTemplatePermissionsCmd;15 +listTemplatePermissions=com.cloud.api.commands.ListTemplatePermissionsCmd;15 + +#### iso commands +attachIso=com.cloud.api.commands.AttachIsoCmd;15 +detachIso=com.cloud.api.commands.DetachIsoCmd;15 +listIsos=com.cloud.api.commands.ListIsosCmd;15 +registerIso=com.cloud.api.commands.RegisterIsoCmd;15 +updateIso=com.cloud.api.commands.UpdateIsoCmd;15 +deleteIso=com.cloud.api.commands.DeleteIsoCmd;15 +copyIso=com.cloud.api.commands.CopyIsoCmd;15 +updateIsoPermissions=com.cloud.api.commands.UpdateIsoPermissionsCmd;15 +listIsoPermissions=com.cloud.api.commands.ListIsoPermissionsCmd;15 + +#### guest OS commands +listOsTypes=com.cloud.api.commands.ListGuestOsCmd;15 +listOsCategories=com.cloud.api.commands.ListGuestOsCategoriesCmd;15 + +#### service offering commands +createServiceOffering=com.cloud.api.commands.CreateServiceOfferingCmd;1 +deleteServiceOffering=com.cloud.api.commands.DeleteServiceOfferingCmd;1 +updateServiceOffering=com.cloud.api.commands.UpdateServiceOfferingCmd;1 +listServiceOfferings=com.cloud.api.commands.ListServiceOfferingsCmd;15 + +#### disk offering commands +createDiskOffering=com.cloud.api.commands.CreateDiskOfferingCmd;1 +updateDiskOffering=com.cloud.api.commands.UpdateDiskOfferingCmd;1 +deleteDiskOffering=com.cloud.api.commands.DeleteDiskOfferingCmd;1 +listDiskOfferings=com.cloud.api.commands.ListDiskOfferingsCmd;15 + +#### vlan commands +createVlanIpRange=com.cloud.api.commands.CreateVlanIpRangeCmd;1 +deleteVlanIpRange=com.cloud.api.commands.DeleteVlanIpRangeCmd;1 +listVlanIpRanges=com.cloud.api.commands.ListVlanIpRangesCmd;1 + +#### address commands +associateIpAddress=com.cloud.api.commands.AssociateIPAddrCmd;15 +disassociateIpAddress=com.cloud.api.commands.DisassociateIPAddrCmd;15 +listPublicIpAddresses=com.cloud.api.commands.ListPublicIpAddressesCmd;15 + +#### firewall commands +createPortForwardingServiceRule=com.cloud.api.commands.CreatePortForwardingServiceRuleCmd;15 +deletePortForwardingServiceRule=com.cloud.api.commands.DeletePortForwardingServiceRuleCmd;15 +listPortForwardingServiceRules=com.cloud.api.commands.ListPortForwardingServiceRulesCmd;15 +createPortForwardingService=com.cloud.api.commands.CreatePortForwardingServiceCmd;15 +deletePortForwardingService=com.cloud.api.commands.DeletePortForwardingServiceCmd;15 +assignPortForwardingService=com.cloud.api.commands.AssignPortForwardingServiceCmd;15 +removePortForwardingService=com.cloud.api.commands.RemovePortForwardingServiceCmd;15 +listPortForwardingServices=com.cloud.api.commands.ListPortForwardingServicesCmd;15 +listPortForwardingServicesByVm=com.cloud.api.commands.ListPortForwardingServicesByVmCmd;15 +listPortForwardingRules=com.cloud.api.commands.ListPortForwardingRulesCmd;15 +createPortForwardingRule=com.cloud.api.commands.CreateIPForwardingRuleCmd;15 +deletePortForwardingRule=com.cloud.api.commands.DeleteIPForwardingRuleCmd;15 +updatePortForwardingRule=com.cloud.api.commands.UpdateIPForwardingRuleCmd;15 + +#### load balancer commands +createLoadBalancerRule=com.cloud.api.commands.CreateLoadBalancerRuleCmd;15 +deleteLoadBalancerRule=com.cloud.api.commands.DeleteLoadBalancerRuleCmd;15 +updateLoadBalancerRule=com.cloud.api.commands.UpdateLoadBalancerRuleCmd;15 +removeFromLoadBalancerRule=com.cloud.api.commands.RemoveFromLoadBalancerRuleCmd;15 +assignToLoadBalancerRule=com.cloud.api.commands.AssignToLoadBalancerRuleCmd;15 +listLoadBalancerRules=com.cloud.api.commands.ListLoadBalancerRulesCmd;15 +listLoadBalancerRuleInstances=com.cloud.api.commands.ListLoadBalancerRuleInstancesCmd;15 + +#### router commands +startRouter=com.cloud.api.commands.StartRouterCmd;3 +rebootRouter=com.cloud.api.commands.RebootRouterCmd;3 +stopRouter=com.cloud.api.commands.StopRouterCmd;3 +listRouters=com.cloud.api.commands.ListRoutersCmd;7 + +#### system vm commands +startSystemVm=com.cloud.api.commands.StartSystemVMCmd;1 +rebootSystemVm=com.cloud.api.commands.RebootSystemVmCmd;1 +stopSystemVm=com.cloud.api.commands.StopSystemVmCmd;1 +listSystemVms=com.cloud.api.commands.ListSystemVMsCmd;1 + +#### configuration commands +updateConfiguration=com.cloud.api.commands.UpdateCfgCmd;1 +listConfigurations=com.cloud.api.commands.ListCfgsByCmd;1 +addConfig=com.cloud.api.commands.AddConfigCmd;15 + +#### pod commands +createPod=com.cloud.api.commands.CreatePodCmd;1 +updatePod=com.cloud.api.commands.UpdatePodCmd;1 +deletePod=com.cloud.api.commands.DeletePodCmd;1 +listPods=com.cloud.api.commands.ListPodsByCmd;1 + +#### zone commands +createZone=com.cloud.api.commands.CreateZoneCmd;1 +updateZone=com.cloud.api.commands.UpdateZoneCmd;1 +deleteZone=com.cloud.api.commands.DeleteZoneCmd;1 +listZones=com.cloud.api.commands.ListZonesByCmd;15 + +#### events commands +listEvents=com.cloud.api.commands.ListEventsCmd;15 + +#### alerts commands +listAlerts=com.cloud.api.commands.ListAlertsCmd;1 + +#### system capacity commands +listCapacity=com.cloud.api.commands.ListCapacityCmd;1 + +#### host commands +addHost=com.cloud.api.commands.AddHostCmd;1 +reconnectHost=com.cloud.api.commands.ReconnectHostCmd;1 +updateHost=com.cloud.api.commands.UpdateHostCmd;1 +deleteHost=com.cloud.api.commands.DeleteHostCmd;1 +prepareHostForMaintenance=com.cloud.api.commands.PrepareForMaintenanceCmd;1 +cancelHostMaintenance=com.cloud.api.commands.CancelMaintenanceCmd;1 +listHosts=com.cloud.api.commands.ListHostsCmd;1 +addSecondaryStorage=com.cloud.api.commands.AddSecondaryStorageCmd;1 + +#### volume commands +attachVolume=com.cloud.api.commands.AttachVolumeCmd;15 +detachVolume=com.cloud.api.commands.DetachVolumeCmd;15 +createVolume=com.cloud.api.commands.CreateVolumeCmd;15 +deleteVolume=com.cloud.api.commands.DeleteVolumeCmd;15 +listVolumes=com.cloud.api.commands.ListVolumesCmd;15 + +#### registration command: FIXME -- this really should be something in management server that +#### generates a new key for the user and they just have to +#### use that key...the key is stored in the db associated w/ +#### the userId...every request to the developer API should be +#### checked against the key +registerUserKeys=com.cloud.api.commands.RegisterCmd;1 + +### async-query command +queryAsyncJobResult=com.cloud.api.commands.QueryAsyncJobResultCmd;15 +listAsyncJobs=com.cloud.api.commands.ListAsyncJobsCmd;15 + +#### storage pools commands +listStoragePools=com.cloud.api.commands.ListStoragePoolsCmd;1 +createStoragePool=com.cloud.api.commands.CreateStoragePoolCmd;1 +updateStoragePool=com.cloud.api.commands.UpdateStoragePoolCmd;1 +deleteStoragePool=com.cloud.api.commands.DeletePoolCmd;1 +listClusters=com.cloud.api.commands.ListClustersCmd;1 +enableStorageMaintenance=com.cloud.api.commands.PreparePrimaryStorageForMaintenanceCmd;1 +cancelStorageMaintenance=com.cloud.api.commands.CancelPrimaryStorageMaintenanceCmd;1 + +#### network group commands +createNetworkGroup=com.cloud.api.commands.CreateNetworkGroupCmd;11 +deleteNetworkGroup=com.cloud.api.commands.DeleteNetworkGroupCmd;11 +authorizeNetworkGroupIngress=com.cloud.api.commands.AuthorizeNetworkGroupIngressCmd;11 +revokeNetworkGroupIngress=com.cloud.api.commands.RevokeNetworkGroupIngressCmd;11 +listNetworkGroups=com.cloud.api.commands.ListNetworkGroupsCmd;11 + +registerPreallocatedLun=com.cloud.server.api.commands.RegisterPreallocatedLunCmd;1 +deletePreallocatedLun=com.cloud.server.api.commands.DeletePreallocatedLunCmd;1 +listPreallocatedLuns=com.cloud.api.commands.ListPreallocatedLunsCmd;1 \ No newline at end of file diff --git a/client/tomcatconf/components.xml.in b/client/tomcatconf/components.xml.in new file mode 100755 index 00000000000..a10c9c2e9cd --- /dev/null +++ b/client/tomcatconf/components.xml.in @@ -0,0 +1,236 @@ + + + + + + + + + + + + 50 + -1 + + + 50 + -1 + + + 50 + -1 + + + 50 + -1 + + + + + + + + + + 50 + -1 + routing + + + 5000 + 300 + + + + 50 + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + 50 + -1 + + + + + + + diff --git a/client/tomcatconf/context.xml.in b/client/tomcatconf/context.xml.in new file mode 100644 index 00000000000..9913dd149a4 --- /dev/null +++ b/client/tomcatconf/context.xml.in @@ -0,0 +1,35 @@ + + + + + + + WEB-INF/web.xml + + + + + + + + diff --git a/client/tomcatconf/db.properties.in b/client/tomcatconf/db.properties.in new file mode 100644 index 00000000000..c944023d7b3 --- /dev/null +++ b/client/tomcatconf/db.properties.in @@ -0,0 +1,40 @@ +# management server clustering parameters, change cluster.node.IP to the machine IP address +# in which the management server(Tomcat) is running +cluster.node.IP=127.0.0.1 +cluster.servlet.port=9090 + +# Cloud.com database settings +db.cloud.username=@DBUSER@ +db.cloud.password=@DBPW@ +db.cloud.host=@DBHOST@ +db.cloud.port=3306 +db.cloud.name=cloud + +# Cloud.com database tuning parameters +db.cloud.maxActive=250 +db.cloud.maxIdle=30 +db.cloud.maxWait=10000 +db.cloud.autoReconnect=true +db.cloud.validationQuery=SELECT 1 +db.cloud.testOnBorrow=true +db.cloud.testWhileIdle=true +db.cloud.timeBetweenEvictionRunsMillis=40000 +db.cloud.minEvictableIdleTimeMillis=240000 +db.cloud.removeAbandoned=false +db.cloud.removeAbandonedTimeout=300 +db.cloud.logAbandoned=true +db.cloud.poolPreparedStatements=false +db.cloud.url.params=prepStmtCacheSize=517&cachePrepStmts=true + +# usage database settings +db.usage.username=@DBUSER@ +db.usage.password=@DBPW@ +db.usage.host=@DBHOST@ +db.usage.port=3306 +db.usage.name=cloud_usage + +# usage database tuning parameters +db.usage.maxActive=100 +db.usage.maxIdle=30 +db.usage.maxWait=10000 +db.usage.autoReconnect=true diff --git a/client/tomcatconf/ehcache.xml.in b/client/tomcatconf/ehcache.xml.in new file mode 100755 index 00000000000..c65deeacdb9 --- /dev/null +++ b/client/tomcatconf/ehcache.xml.in @@ -0,0 +1,527 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/tomcatconf/environment.properties.in b/client/tomcatconf/environment.properties.in new file mode 100644 index 00000000000..e0dc4fb9b40 --- /dev/null +++ b/client/tomcatconf/environment.properties.in @@ -0,0 +1,4 @@ +# management server compile-time environment parameters + +paths.script=@AGENTLIBDIR@ +mount.parent=@MSMNTDIR@ diff --git a/client/tomcatconf/log4j-cloud.xml.in b/client/tomcatconf/log4j-cloud.xml.in new file mode 100755 index 00000000000..b9bab1b0491 --- /dev/null +++ b/client/tomcatconf/log4j-cloud.xml.in @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/tomcatconf/logging.properties.in b/client/tomcatconf/logging.properties.in new file mode 100644 index 00000000000..68be2d7f457 --- /dev/null +++ b/client/tomcatconf/logging.properties.in @@ -0,0 +1,64 @@ +# 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. + +handlers = 1catalina.org.apache.juli.FileHandler, 2localhost.org.apache.juli.FileHandler, 3manager.org.apache.juli.FileHandler, 4host-manager.org.apache.juli.FileHandler, java.util.logging.ConsoleHandler + +.handlers = 1catalina.org.apache.juli.FileHandler, java.util.logging.ConsoleHandler + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +1catalina.org.apache.juli.FileHandler.level = FINE +1catalina.org.apache.juli.FileHandler.directory = ${catalina.base}/logs +1catalina.org.apache.juli.FileHandler.prefix = catalina. + +2localhost.org.apache.juli.FileHandler.level = FINE +2localhost.org.apache.juli.FileHandler.directory = ${catalina.base}/logs +2localhost.org.apache.juli.FileHandler.prefix = localhost. + +3manager.org.apache.juli.FileHandler.level = FINE +3manager.org.apache.juli.FileHandler.directory = ${catalina.base}/logs +3manager.org.apache.juli.FileHandler.prefix = manager. + +4host-manager.org.apache.juli.FileHandler.level = FINE +4host-manager.org.apache.juli.FileHandler.directory = ${catalina.base}/logs +4host-manager.org.apache.juli.FileHandler.prefix = host-manager. + +java.util.logging.ConsoleHandler.level = FINE +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter + + +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.FileHandler + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].handlers = 3manager.org.apache.juli.FileHandler + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].handlers = 4host-manager.org.apache.juli.FileHandler + +# For example, set the com.xyz.foo logger to only log SEVERE +# messages: +#org.apache.catalina.startup.ContextConfig.level = FINE +#org.apache.catalina.startup.HostConfig.level = FINE +#org.apache.catalina.session.ManagerBase.level = FINE +#org.apache.catalina.core.AprLifecycleListener.level=FINE diff --git a/client/tomcatconf/server.xml.in b/client/tomcatconf/server.xml.in new file mode 100755 index 00000000000..99ea003791c --- /dev/null +++ b/client/tomcatconf/server.xml.in @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/tomcatconf/tomcat-users.xml.in b/client/tomcatconf/tomcat-users.xml.in new file mode 100644 index 00000000000..81422a02892 --- /dev/null +++ b/client/tomcatconf/tomcat-users.xml.in @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/client/tomcatconf/tomcat6.conf.in b/client/tomcatconf/tomcat6.conf.in new file mode 100644 index 00000000000..83b3be702f8 --- /dev/null +++ b/client/tomcatconf/tomcat6.conf.in @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# System-wide configuration file for tomcat6 services +# This will be sourced by tomcat6 and any secondary service +# Values will be overridden by service-specific configuration +# files in /etc/sysconfig +# +# Use this one to change default values for all services +# Change the service specific ones to affect only one service +# (see, for instance, /etc/sysconfig/tomcat6) +# + +# Where your java installation lives +#JAVA_HOME="/usr/lib/jvm/java" + +# Where your tomcat installation lives +CATALINA_BASE="@MSENVIRON@" +CATALINA_HOME="@MSENVIRON@" +JASPER_HOME="@MSENVIRON@" +CATALINA_TMPDIR="@MSENVIRON@/temp" + +# You can pass some parameters to java here if you wish to +#JAVA_OPTS="-Xminf0.1 -Xmaxf0.3" + +# Use JAVA_OPTS to set java.library.path for libtcnative.so +#JAVA_OPTS="-Djava.library.path=/usr/lib64" +JAVA_OPTS="-Djava.awt.headless=true -Xmx128M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=@MSLOGDIR@" + +# What user should run tomcat +TOMCAT_USER="@MSUSER@" +# Do not remove the following line +TOMCAT6_USER="$TOMCAT_USER" + +TOMCAT_LOG="@MSLOGDIR@/catalina.out" + +# You can change your tomcat locale here +#LANG="en_US" + +# Run tomcat under the Java Security Manager +SECURITY_MANAGER="false" + +# Time to wait in seconds, before killing process +SHUTDOWN_WAIT="30" + +# Whether to annoy the user with "attempting to shut down" messages or not +SHUTDOWN_VERBOSE="false" + +# Set the TOMCAT_PID location +CATALINA_PID="@PIDDIR@/@PACKAGE@-management.pid" + +# Connector port is 8080 for this tomcat6 instance +#CONNECTOR_PORT="8080" + +# We pick up the classpath in the next line + +dummy=1 ; . @MSCONF@/classpath.conf diff --git a/client/tomcatconf/web.xml.in b/client/tomcatconf/web.xml.in new file mode 100644 index 00000000000..44b6eab07fe --- /dev/null +++ b/client/tomcatconf/web.xml.in @@ -0,0 +1,1188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + default + org.apache.catalina.servlets.DefaultServlet + + debug + 0 + + + listings + false + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jsp + org.apache.jasper.servlet.JspServlet + + fork + false + + + xpoweredBy + false + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + default + / + + + + + + + + jsp + *.jsp + + + + jsp + *.jspx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 30 + + + + + + + + + + + + abs + audio/x-mpeg + + + ai + application/postscript + + + aif + audio/x-aiff + + + aifc + audio/x-aiff + + + aiff + audio/x-aiff + + + aim + application/x-aim + + + art + image/x-jg + + + asf + video/x-ms-asf + + + asx + video/x-ms-asf + + + au + audio/basic + + + avi + video/x-msvideo + + + avx + video/x-rad-screenplay + + + bcpio + application/x-bcpio + + + bin + application/octet-stream + + + bmp + image/bmp + + + body + text/html + + + cdf + application/x-cdf + + + cer + application/x-x509-ca-cert + + + class + application/java + + + cpio + application/x-cpio + + + csh + application/x-csh + + + css + text/css + + + dib + image/bmp + + + doc + application/msword + + + dtd + application/xml-dtd + + + dv + video/x-dv + + + dvi + application/x-dvi + + + eps + application/postscript + + + etx + text/x-setext + + + exe + application/octet-stream + + + gif + image/gif + + + gtar + application/x-gtar + + + gz + application/x-gzip + + + hdf + application/x-hdf + + + hqx + application/mac-binhex40 + + + htc + text/x-component + + + htm + text/html + + + html + text/html + + + hqx + application/mac-binhex40 + + + ief + image/ief + + + jad + text/vnd.sun.j2me.app-descriptor + + + jar + application/java-archive + + + java + text/plain + + + jnlp + application/x-java-jnlp-file + + + jpe + image/jpeg + + + jpeg + image/jpeg + + + jpg + image/jpeg + + + js + text/javascript + + + jsf + text/plain + + + jspf + text/plain + + + kar + audio/x-midi + + + latex + application/x-latex + + + m3u + audio/x-mpegurl + + + mac + image/x-macpaint + + + man + application/x-troff-man + + + mathml + application/mathml+xml + + + me + application/x-troff-me + + + mid + audio/x-midi + + + midi + audio/x-midi + + + mif + application/x-mif + + + mov + video/quicktime + + + movie + video/x-sgi-movie + + + mp1 + audio/x-mpeg + + + mp2 + audio/x-mpeg + + + mp3 + audio/x-mpeg + + + mp4 + video/mp4 + + + mpa + audio/x-mpeg + + + mpe + video/mpeg + + + mpeg + video/mpeg + + + mpega + audio/x-mpeg + + + mpg + video/mpeg + + + mpv2 + video/mpeg2 + + + ms + application/x-wais-source + + + nc + application/x-netcdf + + + oda + application/oda + + + + odb + application/vnd.oasis.opendocument.database + + + + odc + application/vnd.oasis.opendocument.chart + + + + odf + application/vnd.oasis.opendocument.formula + + + + odg + application/vnd.oasis.opendocument.graphics + + + + odi + application/vnd.oasis.opendocument.image + + + + odm + application/vnd.oasis.opendocument.text-master + + + + odp + application/vnd.oasis.opendocument.presentation + + + + ods + application/vnd.oasis.opendocument.spreadsheet + + + + odt + application/vnd.oasis.opendocument.text + + + ogg + application/ogg + + + + otg + application/vnd.oasis.opendocument.graphics-template + + + + oth + application/vnd.oasis.opendocument.text-web + + + + otp + application/vnd.oasis.opendocument.presentation-template + + + + ots + application/vnd.oasis.opendocument.spreadsheet-template + + + + ott + application/vnd.oasis.opendocument.text-template + + + pbm + image/x-portable-bitmap + + + pct + image/pict + + + pdf + application/pdf + + + pgm + image/x-portable-graymap + + + pic + image/pict + + + pict + image/pict + + + pls + audio/x-scpls + + + png + image/png + + + pnm + image/x-portable-anymap + + + pnt + image/x-macpaint + + + ppm + image/x-portable-pixmap + + + ppt + application/vnd.ms-powerpoint + + + pps + application/vnd.ms-powerpoint + + + ps + application/postscript + + + psd + image/x-photoshop + + + qt + video/quicktime + + + qti + image/x-quicktime + + + qtif + image/x-quicktime + + + ras + image/x-cmu-raster + + + rdf + application/rdf+xml + + + rgb + image/x-rgb + + + rm + application/vnd.rn-realmedia + + + roff + application/x-troff + + + rtf + application/rtf + + + rtx + text/richtext + + + sh + application/x-sh + + + shar + application/x-shar + + + + smf + audio/x-midi + + + sit + application/x-stuffit + + + snd + audio/basic + + + src + application/x-wais-source + + + sv4cpio + application/x-sv4cpio + + + sv4crc + application/x-sv4crc + + + svg + image/svg+xml + + + svgz + image/svg+xml + + + swf + application/x-shockwave-flash + + + t + application/x-troff + + + tar + application/x-tar + + + tcl + application/x-tcl + + + tex + application/x-tex + + + texi + application/x-texinfo + + + texinfo + application/x-texinfo + + + tif + image/tiff + + + tiff + image/tiff + + + tr + application/x-troff + + + tsv + text/tab-separated-values + + + txt + text/plain + + + ulw + audio/basic + + + ustar + application/x-ustar + + + vxml + application/voicexml+xml + + + xbm + image/x-xbitmap + + + xht + application/xhtml+xml + + + xhtml + application/xhtml+xml + + + xls + application/vnd.ms-excel + + + xml + application/xml + + + xpm + image/x-xpixmap + + + xsl + application/xml + + + xslt + application/xslt+xml + + + xul + application/vnd.mozilla.xul+xml + + + xwd + image/x-xwindowdump + + + vsd + application/x-visio + + + wav + audio/x-wav + + + + wbmp + image/vnd.wap.wbmp + + + + wml + text/vnd.wap.wml + + + + wmlc + application/vnd.wap.wmlc + + + + wmls + text/vnd.wap.wmlscript + + + + wmlscriptc + application/vnd.wap.wmlscriptc + + + wmv + video/x-ms-wmv + + + wrl + x-world/x-vrml + + + wspolicy + application/wspolicy+xml + + + Z + application/x-compress + + + z + application/x-compress + + + zip + application/zip + + + + + + + + + + + + + + + + index.html + index.htm + index.jsp + + + diff --git a/cloud.spec b/cloud.spec new file mode 100644 index 00000000000..d7643ec5485 --- /dev/null +++ b/cloud.spec @@ -0,0 +1,765 @@ +%define __os_install_post %{nil} +%global debug_package %{nil} + +# DISABLE the post-percentinstall java repacking and line number stripping +# we need to find a way to just disable the java repacking and line number stripping, but not the autodeps + +%define _ver 2.1.97 +%define _rel 1 + +Name: cloud +Summary: Cloud.com Stack +Version: %{_ver} +#http://fedoraproject.org/wiki/PackageNamingGuidelines#Pre-Release_packages +%if "%{?_prerelease}" != "" +Release: 0.%{_build_number}%{_prerelease} +%else +Release: %{_rel} +%endif +License: GPLv3+ with exceptions or CSL 1.1 +Vendor: Cloud.com, Inc. +Packager: Manuel Amador (Rudd-O) +Group: System Environment/Libraries +# FIXME do groups for every single one of the subpackages +Source0: %{name}-%{_ver}.tar.bz2 +BuildRoot: %{_tmppath}/%{name}-%{_ver}-%{release}-build + +BuildRequires: java-1.6.0-openjdk-devel +BuildRequires: tomcat6 +BuildRequires: ws-commons-util +#BuildRequires: commons-codec +BuildRequires: commons-dbcp +BuildRequires: commons-collections +BuildRequires: commons-httpclient +BuildRequires: jpackage-utils +BuildRequires: gcc +BuildRequires: glibc-devel + +%global _premium %(tar jtvmf %{SOURCE0} '*/premium/' --occurrence=1 2>/dev/null | wc -l) + +%description +This is the Cloud.com Stack, a highly-scalable elastic, open source, +intelligent cloud implementation. + +%package utils +Summary: Cloud.com utility library +Requires: java >= 1.6.0 +Group: System Environment/Libraries +Obsoletes: vmops-utils < %{version}-%{release} +%description utils +The Cloud.com utility libraries provide a set of Java classes used +in the Cloud.com Stack. + +%package client-ui +Summary: Cloud.com management server UI +Requires: %{name}-client +Group: System Environment/Libraries +Obsoletes: vmops-client-ui < %{version}-%{release} +%description client-ui +The Cloud.com management server is the central point of coordination, +management, and intelligence in the Cloud.com Stack. This package +is a requirement of the %{name}-client package, which installs the +Cloud.com management server. + +%package server +Summary: Cloud.com server library +Requires: java >= 1.6.0 +Obsoletes: vmops-server < %{version}-%{release} +Requires: %{name}-utils = %{version}-%{release}, %{name}-core = %{version}-%{release}, %{name}-deps = %{version}-%{release}, tomcat6-servlet-2.5-api +Group: System Environment/Libraries +%description server +The Cloud.com server libraries provide a set of Java classes used +in the Cloud.com Stack. + +%package vnet +Summary: Cloud.com-specific virtual network daemon +Requires: python +Requires: %{name}-daemonize = %{version}-%{release} +Requires: %{name}-python = %{version}-%{release} +Requires: net-tools +Requires: bridge-utils +Obsoletes: vmops-vnet < %{version}-%{release} +Group: System Environment/Daemons +%description vnet +The Cloud.com virtual network daemon manages virtual networks used in the +Cloud.com Stack. + +%package agent-scripts +Summary: Cloud.com agent scripts +# FIXME nuke the archdependency +Requires: python +Requires: bash +Requires: bzip2 +Requires: gzip +Requires: unzip +Requires: /sbin/mount.nfs +Requires: openssh-clients +Requires: nfs-utils +Obsoletes: vmops-agent-scripts < %{version}-%{release} +Group: System Environment/Libraries +%description agent-scripts +The Cloud.com agent is in charge of managing shared computing resources in +a Cloud.com Stack-powered cloud. Install this package if this computer +will participate in your cloud -- this is a requirement for the Cloud.com +agent. + +%package python +Summary: Cloud.com Python library +# FIXME nuke the archdependency +Requires: python +Group: System Environment/Libraries +%description python +The Cloud.com Python library contains a few Python modules that the +CloudStack uses. + +%package deps +Summary: Cloud.com library dependencies +Requires: java >= 1.6.0 +Obsoletes: vmops-deps < %{version}-%{release} +Group: System Environment/Libraries +%description deps +This package contains a number of third-party dependencies +not shipped by distributions, required to run the Cloud.com +Stack. + +%package daemonize +Summary: Cloud.com daemonization utility +Group: System Environment/Libraries +Obsoletes: vmops-daemonize < %{version}-%{release} +%description daemonize +This package contains a program that daemonizes the specified +process. The Cloud.com Cloud Stack uses this to start the agent +as a service. + +%package core +Summary: Cloud.com core library +Requires: java >= 1.6.0 +Requires: %{name}-utils = %{version}-%{release}, %{name}-deps = %{version}-%{release} +Group: System Environment/Libraries +Obsoletes: vmops-core < %{version}-%{release} +%description core +The Cloud.com core libraries provide a set of Java classes used +in the Cloud.com Stack. + +%package client +Summary: Cloud.com management server +# If GCJ is present, a setPerformanceSomething method fails to load Catalina +Conflicts: java-1.5.0-gcj-devel +Obsoletes: vmops-client < %{version}-%{release} +Requires: java >= 1.6.0 +Requires: %{name}-deps = %{version}-%{release}, %{name}-utils = %{version}-%{release}, %{name}-server = %{version}-%{release} +Requires: %{name}-client-ui = %{version}-%{release} +Requires: %{name}-setup = %{version}-%{release} +# reqs the agent-scripts package because of xenserver within the management server +Requires: %{name}-agent-scripts = %{version}-%{release} +Requires: %{name}-python = %{version}-%{release} +# for consoleproxy +# Requires: %{name}-agent +Requires: tomcat6 +Requires: ws-commons-util +#Requires: commons-codec +Requires: commons-dbcp +Requires: commons-collections +Requires: commons-httpclient +Requires: jpackage-utils +Requires: sudo +Requires: /sbin/service +Requires: /sbin/chkconfig +Requires: /usr/bin/ssh-keygen +Requires: MySQL-python +Requires: python-paramiko +Requires: augeas >= 0.7.1 +Group: System Environment/Libraries +%description client +The Cloud.com management server is the central point of coordination, +management, and intelligence in the Cloud.com Stack. This package +installs the management server.. + +%package setup +Summary: Cloud.com setup tools +Obsoletes: vmops-setup < %{version}-%{release} +Requires: java >= 1.6.0 +Requires: python +Requires: mysql +Requires: %{name}-utils = %{version}-%{release} +Requires: %{name}-server = %{version}-%{release} +Requires: %{name}-deps = %{version}-%{release} +Requires: %{name}-python = %{version}-%{release} +Requires: MySQL-python +Group: System Environment/Libraries +%description setup +The Cloud.com setup tools let you set up your Management Server and Usage Server. + +%package agent-libs +Summary: Cloud.com agent libraries +Requires: java >= 1.6.0 +Requires: %{name}-utils = %{version}-%{release}, %{name}-core = %{version}-%{release}, %{name}-deps = %{version}-%{release} +Requires: commons-httpclient +#Requires: commons-codec +Requires: commons-collections +Requires: commons-pool +Requires: commons-dbcp +Requires: jakarta-commons-logging +Requires: jpackage-utils +Group: System Environment/Libraries +%description agent-libs +The Cloud.com agent libraries are used by the Cloud Agent and the Cloud +Console Proxy. + +%package agent +Summary: Cloud.com agent +Obsoletes: vmops-agent < %{version}-%{release} +Obsoletes: vmops-console < %{version}-%{release} +Obsoletes: cloud-console < %{version}-%{release} +Requires: java >= 1.6.0 +Requires: %{name}-utils = %{version}-%{release}, %{name}-core = %{version}-%{release}, %{name}-deps = %{version}-%{release} +Requires: %{name}-agent-libs = %{version}-%{release} +Requires: %{name}-agent-scripts = %{version}-%{release} +Requires: %{name}-vnet = %{version}-%{release} +Requires: python +Requires: %{name}-python = %{version}-%{release} +Requires: commons-httpclient +#Requires: commons-codec +Requires: commons-collections +Requires: commons-pool +Requires: commons-dbcp +Requires: jakarta-commons-logging +Requires: libvirt +Requires: /usr/sbin/libvirtd +Requires: jpackage-utils +Requires: %{name}-daemonize +Requires: /sbin/service +Requires: /sbin/chkconfig +Requires: kvm +Requires: libcgroup +Requires: /usr/bin/uuidgen +Requires: augeas >= 0.7.1 +Requires: rsync +Requires: /bin/egrep +Requires: /sbin/ip +Requires: vconfig +Group: System Environment/Libraries +%description agent +The Cloud.com agent is in charge of managing shared computing resources in +a Cloud.com Stack-powered cloud. Install this package if this computer +will participate in your cloud. + +%package console-proxy +Summary: Cloud.com console proxy +Requires: java >= 1.6.0 +Requires: %{name}-utils = %{version}-%{release}, %{name}-core = %{version}-%{release}, %{name}-deps = %{version}-%{release}, %{name}-agent-libs = %{version}-%{release} +Requires: python +Requires: %{name}-python = %{version}-%{release} +Requires: commons-httpclient +#Requires: commons-codec +Requires: commons-collections +Requires: commons-pool +Requires: commons-dbcp +Requires: jakarta-commons-logging +Requires: jpackage-utils +Requires: %{name}-daemonize +Requires: /sbin/service +Requires: /sbin/chkconfig +Requires: /usr/bin/uuidgen +Requires: augeas >= 0.7.1 +Requires: /bin/egrep +Requires: /sbin/ip +Group: System Environment/Libraries +%description console-proxy +The Cloud.com console proxy is the service in charge of granting console +access into virtual machines managed by the Cloud.com CloudStack. + + +%if %{_premium} + +%package test +Summary: Cloud.com test suite +Requires: java >= 1.6.0 +Requires: %{name}-utils = %{version}-%{release}, %{name}-deps = %{version}-%{release}, wget +Group: System Environment/Libraries +Obsoletes: vmops-test < %{version}-%{release} +%description test +The Cloud.com test package contains a suite of automated tests +that the very much appreciated QA team at Cloud.com constantly +uses to help increase the quality of the Cloud.com Stack. + +%package premium-deps +Summary: Cloud.com premium library dependencies +Requires: java >= 1.6.0 +Provides: %{name}-deps = %{version}-%{release} +Group: System Environment/Libraries +Obsoletes: vmops-premium-deps < %{version}-%{release} +%description premium-deps +This package contains the certified software components required to run +the premium edition of the Cloud.com Stack. + +%package premium +Summary: Cloud.com premium components +Obsoletes: vmops-premium < %{version}-%{release} +Provides: %{name}-premium-plugin-zynga = %{version}-%{release} +Obsoletes: %{name}-premium-plugin-zynga < %{version}-%{release} +Provides: %{name}-premium-vendor-zynga = %{version}-%{release} +Obsoletes: %{name}-premium-vendor-zynga < %{version}-%{release} +Requires: java >= 1.6.0 +Requires: %{name}-utils = %{version}-%{release} +Requires: %{name}-premium-deps +License: CSL 1.1 +Group: System Environment/Libraries +%description premium +The Cloud.com premium components expand the range of features on your Cloud.com Stack. + +%package usage +Summary: Cloud.com usage monitor +Obsoletes: vmops-usage < %{version}-%{release} +Requires: java >= 1.6.0 +Requires: %{name}-utils = %{version}-%{release}, %{name}-core = %{version}-%{release}, %{name}-deps = %{version}-%{release}, %{name}-server = %{version}-%{release}, %{name}-premium = %{version}-%{release}, %{name}-daemonize = %{version}-%{release} +Requires: %{name}-setup = %{version}-%{release} +Requires: %{name}-client = %{version}-%{release} +License: CSL 1.1 +Group: System Environment/Libraries +%description usage +The Cloud.com usage monitor provides usage accounting across the entire cloud for +cloud operators to charge based on usage parameters. + +%endif + +%prep + +%if %{_premium} +echo Doing premium build +%else +echo Doing open source build +%endif + +%setup -q -n %{name}-%{_ver} + +%build + +# this fixes the /usr/com bug on centos5 +%define _localstatedir /var +%define _sharedstatedir /var/lib +./waf configure --prefix=%{_prefix} --libdir=%{_libdir} --bindir=%{_bindir} --javadir=%{_javadir} --sharedstatedir=%{_sharedstatedir} --localstatedir=%{_localstatedir} --sysconfdir=%{_sysconfdir} --mandir=%{_mandir} --docdir=%{_docdir}/%{name}-%{version} --with-tomcat=%{_datadir}/tomcat6 --tomcat-user=%{name} --fast +./waf build --build-number=%{?_build_number} + +%install +[ ${RPM_BUILD_ROOT} != "/" ] && rm -rf ${RPM_BUILD_ROOT} +# we put the build number again here, otherwise state checking will cause an almost-full recompile +./waf install --destdir=$RPM_BUILD_ROOT --nochown --build-number=%{?_build_number} + +%clean + +[ ${RPM_BUILD_ROOT} != "/" ] && rm -rf ${RPM_BUILD_ROOT} + + +%preun client +/sbin/service %{name}-management stop || true +if [ "$1" == "0" ] ; then + /sbin/chkconfig --del %{name}-management > /dev/null 2>&1 || true + /sbin/service %{name}-management stop > /dev/null 2>&1 || true +fi + +%pre client +id %{name} > /dev/null 2>&1 || /usr/sbin/useradd -M -c "Cloud.com unprivileged user" \ + -r -s /bin/sh -d %{_sharedstatedir}/%{name}/management %{name}|| true +# user harcoded here, also hardcoded on wscript + +%post client +if [ "$1" == "1" ] ; then + /sbin/chkconfig --add %{name}-management > /dev/null 2>&1 || true + /sbin/chkconfig --level 345 %{name}-management on > /dev/null 2>&1 || true +fi +test -f %{_sharedstatedir}/%{name}/management/.ssh/id_rsa || su - %{name} -c 'yes "" 2>/dev/null | ssh-keygen -t rsa -q -N ""' < /dev/null + + + +%if %{_premium} + +%preun usage +if [ "$1" == "0" ] ; then + /sbin/chkconfig --del %{name}-usage > /dev/null 2>&1 || true + /sbin/service %{name}-usage stop > /dev/null 2>&1 || true +fi + +%pre usage +id %{name} > /dev/null 2>&1 || /usr/sbin/useradd -M -c "Cloud.com unprivileged user" \ + -r -s /bin/sh -d %{_sharedstatedir}/%{name}/management %{name}|| true +# user harcoded here, also hardcoded on wscript + +%post usage +if [ "$1" == "1" ] ; then + /sbin/chkconfig --add %{name}-usage > /dev/null 2>&1 || true + /sbin/chkconfig --level 345 %{name}-usage on > /dev/null 2>&1 || true +else + /sbin/service %{name}-usage condrestart >/dev/null 2>&1 || true +fi + +%endif + +%pre agent-scripts +id %{name} > /dev/null 2>&1 || /usr/sbin/useradd -M -c "Cloud.com unprivileged user" \ + -r -s /bin/sh -d %{_sharedstatedir}/%{name}/management %{name}|| true + + +%preun agent +if [ "$1" == "0" ] ; then + /sbin/chkconfig --del %{name}-agent > /dev/null 2>&1 || true + /sbin/service %{name}-agent stop > /dev/null 2>&1 || true +fi + +%post agent +if [ "$1" == "1" ] ; then + /sbin/chkconfig --add %{name}-agent > /dev/null 2>&1 || true + /sbin/chkconfig --level 345 %{name}-agent on > /dev/null 2>&1 || true +else + /sbin/service %{name}-agent condrestart >/dev/null 2>&1 || true +fi + +%preun console-proxy +if [ "$1" == "0" ] ; then + /sbin/chkconfig --del %{name}-console-proxy > /dev/null 2>&1 || true + /sbin/service %{name}-console-proxy stop > /dev/null 2>&1 || true +fi + +%post console-proxy +if [ "$1" == "1" ] ; then + /sbin/chkconfig --add %{name}-console-proxy > /dev/null 2>&1 || true + /sbin/chkconfig --level 345 %{name}-console-proxy on > /dev/null 2>&1 || true +else + /sbin/service %{name}-console-proxy condrestart >/dev/null 2>&1 || true +fi + +%preun vnet +if [ "$1" == "0" ] ; then + /sbin/chkconfig --del %{name}-vnetd > /dev/null 2>&1 || true + /sbin/service %{name}-vnetd stop > /dev/null 2>&1 || true +fi + +%post vnet +if [ "$1" == "1" ] ; then + /sbin/chkconfig --add %{name}-vnetd > /dev/null 2>&1 || true + /sbin/chkconfig --level 345 %{name}-vnetd on > /dev/null 2>&1 || true +else + /sbin/service %{name}-vnetd condrestart >/dev/null 2>&1 || true +fi + + +%files utils +%defattr(0644,root,root,0755) +%{_javadir}/%{name}-utils.jar +%{_javadir}/%{name}-api.jar +%doc %{_docdir}/%{name}-%{version}/sccs-info +%doc %{_docdir}/%{name}-%{version}/version-info +%doc %{_docdir}/%{name}-%{version}/configure-info +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files client-ui +%defattr(0644,root,root,0755) +%{_datadir}/%{name}/management/webapps/client/* +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files server +%defattr(0644,root,root,0755) +%{_javadir}/%{name}-server.jar +%{_sysconfdir}/%{name}/server/* +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%if %{_premium} + +%files agent-scripts +%defattr(-,root,root,-) +%{_libdir}/%{name}/agent/scripts/* +%{_libdir}/%{name}/agent/vms/systemvm.zip +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%else + +%files agent-scripts +%defattr(-,root,root,-) +%{_libdir}/%{name}/agent/scripts/installer/* +%{_libdir}/%{name}/agent/scripts/network/domr/*.sh +%{_libdir}/%{name}/agent/scripts/storage/*.sh +%{_libdir}/%{name}/agent/scripts/storage/zfs/* +%{_libdir}/%{name}/agent/scripts/storage/qcow2/* +%{_libdir}/%{name}/agent/scripts/storage/secondary/* +%{_libdir}/%{name}/agent/scripts/util/* +%{_libdir}/%{name}/agent/scripts/vm/*.sh +%{_libdir}/%{name}/agent/scripts/vm/storage/nfs/* +%{_libdir}/%{name}/agent/scripts/vm/storage/iscsi/* +%{_libdir}/%{name}/agent/scripts/vm/network/* +%{_libdir}/%{name}/agent/scripts/vm/hypervisor/*.sh +%{_libdir}/%{name}/agent/scripts/vm/hypervisor/kvm/* +%{_libdir}/%{name}/agent/scripts/vm/hypervisor/xen/* +%{_libdir}/%{name}/agent/vms/systemvm.zip +%{_libdir}/%{name}/agent/scripts/vm/hypervisor/xenserver/* +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%endif + +%files daemonize +%defattr(-,root,root,-) +%attr(755,root,root) %{_bindir}/%{name}-daemonize +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files deps +%defattr(0644,root,root,0755) +%{_javadir}/%{name}-commons-codec-1.4.jar +%{_javadir}/%{name}-apache-log4j-extras-1.0.jar +%{_javadir}/%{name}-backport-util-concurrent-3.0.jar +%{_javadir}/%{name}-ehcache.jar +%{_javadir}/%{name}-email.jar +%{_javadir}/%{name}-gson-1.3.jar +%{_javadir}/%{name}-httpcore-4.0.jar +%{_javadir}/%{name}-jna.jar +%{_javadir}/%{name}-junit-4.8.1.jar +%{_javadir}/%{name}-libvirt-0.4.5.jar +%{_javadir}/%{name}-log4j.jar +%{_javadir}/%{name}-trilead-ssh2-build213.jar +%{_javadir}/%{name}-cglib.jar +%{_javadir}/%{name}-mysql-connector-java-5.1.7-bin.jar +%{_javadir}/%{name}-xenserver-5.5.0-1.jar +%{_javadir}/%{name}-xmlrpc-common-3.*.jar +%{_javadir}/%{name}-xmlrpc-client-3.*.jar +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files core +%defattr(0644,root,root,0755) +%{_javadir}/%{name}-core.jar +%doc README +%doc INSTALL +%doc HACKING +%doc debian/copyright + +%files vnet +%defattr(0644,root,root,0755) +%attr(0755,root,root) %{_sbindir}/%{name}-vnetd +%attr(0755,root,root) %{_sbindir}/%{name}-vn +%attr(0755,root,root) %{_initrddir}/%{name}-vnetd +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files python +%defattr(0644,root,root,0755) +%{_prefix}/lib*/python*/site-packages/%{name}* +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files setup +%attr(0755,root,root) %{_bindir}/%{name}-setup-databases +%attr(0755,root,root) %{_bindir}/%{name}-migrate-databases +%dir %{_datadir}/%{name}/setup +%{_datadir}/%{name}/setup/create-database.sql +%{_datadir}/%{name}/setup/create-index-fk.sql +%{_datadir}/%{name}/setup/create-schema.sql +%{_datadir}/%{name}/setup/server-setup.sql +%{_datadir}/%{name}/setup/templates.kvm.sql +%{_datadir}/%{name}/setup/templates.xenserver.sql +%{_datadir}/%{name}/setup/deploy-db-dev.sh +%{_datadir}/%{name}/setup/server-setup.xml +%{_datadir}/%{name}/setup/data-20to21.sql +%{_datadir}/%{name}/setup/index-20to21.sql +%{_datadir}/%{name}/setup/postprocess-20to21.sql +%{_datadir}/%{name}/setup/schema-20to21.sql +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files client +%defattr(0644,root,root,0755) +%{_sysconfdir}/%{name}/management/catalina.policy +%{_sysconfdir}/%{name}/management/catalina.properties +%{_sysconfdir}/%{name}/management/commands.properties +%{_sysconfdir}/%{name}/management/components.xml +%{_sysconfdir}/%{name}/management/context.xml +%config(noreplace) %attr(640,root,%{name}) %{_sysconfdir}/%{name}/management/db.properties +%{_sysconfdir}/%{name}/management/environment.properties +%{_sysconfdir}/%{name}/management/ehcache.xml +%config(noreplace) %{_sysconfdir}/%{name}/management/log4j-%{name}.xml +%{_sysconfdir}/%{name}/management/logging.properties +%{_sysconfdir}/%{name}/management/server.xml +%config(noreplace) %{_sysconfdir}/%{name}/management/tomcat6.conf +%{_sysconfdir}/%{name}/management/classpath.conf +%{_sysconfdir}/%{name}/management/tomcat-users.xml +%{_sysconfdir}/%{name}/management/web.xml +%dir %attr(770,root,%{name}) %{_sysconfdir}/%{name}/management/Catalina +%dir %attr(770,root,%{name}) %{_sysconfdir}/%{name}/management/Catalina/localhost +%dir %attr(770,root,%{name}) %{_sysconfdir}/%{name}/management/Catalina/localhost/client +%config %{_sysconfdir}/sysconfig/%{name}-management +%attr(0755,root,root) %{_initrddir}/%{name}-management +%dir %{_datadir}/%{name}/management +%{_datadir}/%{name}/management/bin +%{_datadir}/%{name}/management/conf +%{_datadir}/%{name}/management/lib +%{_datadir}/%{name}/management/logs +%{_datadir}/%{name}/management/temp +%{_datadir}/%{name}/management/work +%attr(755,root,root) %{_bindir}/%{name}-setup-management +%attr(755,root,root) %{_bindir}/%{name}-update-xenserver-licenses +%dir %attr(770,root,%{name}) %{_sharedstatedir}/%{name}/mnt +%dir %attr(770,%{name},%{name}) %{_sharedstatedir}/%{name}/management +%dir %attr(770,root,%{name}) %{_localstatedir}/cache/%{name}/management +%dir %attr(770,root,%{name}) %{_localstatedir}/cache/%{name}/management/work +%dir %attr(770,root,%{name}) %{_localstatedir}/cache/%{name}/management/temp +%dir %attr(770,root,%{name}) %{_localstatedir}/log/%{name}/management +%dir %attr(770,root,%{name}) %{_localstatedir}/log/%{name}/agent +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files agent-libs +%defattr(0644,root,root,0755) +%{_javadir}/%{name}-agent.jar +%doc README +%doc INSTALL +%doc HACKING +%doc debian/copyright + +%files agent +%defattr(0644,root,root,0755) +%config(noreplace) %{_sysconfdir}/%{name}/agent/agent.properties +%config %{_sysconfdir}/%{name}/agent/developer.properties.template +%config %{_sysconfdir}/%{name}/agent/environment.properties +%config(noreplace) %{_sysconfdir}/%{name}/agent/log4j-%{name}.xml +%attr(0755,root,root) %{_initrddir}/%{name}-agent +%attr(0755,root,root) %{_libexecdir}/agent-runner +%{_libdir}/%{name}/agent/css +%{_libdir}/%{name}/agent/ui +%{_libdir}/%{name}/agent/js +%{_libdir}/%{name}/agent/images +%attr(0755,root,root) %{_bindir}/%{name}-setup-agent +%dir %attr(770,root,root) %{_localstatedir}/log/%{name}/agent +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files console-proxy +%defattr(0644,root,root,0755) +%{_javadir}/%{name}-console*.jar +%config(noreplace) %{_sysconfdir}/%{name}/console-proxy/agent.properties +%config(noreplace) %{_sysconfdir}/%{name}/console-proxy/consoleproxy.properties +%config(noreplace) %{_sysconfdir}/%{name}/console-proxy/log4j-%{name}.xml +%attr(0755,root,root) %{_initrddir}/%{name}-console-proxy +%attr(0755,root,root) %{_libexecdir}/console-proxy-runner +%{_libdir}/%{name}/console-proxy/* +%attr(0755,root,root) %{_bindir}/%{name}-setup-console-proxy +%dir %attr(770,root,root) %{_localstatedir}/log/%{name}/console-proxy +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%if %{_premium} + +%files test +%defattr(0644,root,root,0755) +%attr(755,root,root) %{_bindir}/%{name}-run-test +%{_javadir}/%{name}-test.jar +%{_sharedstatedir}/%{name}/test/* +%{_libdir}/%{name}/test/* +%{_sysconfdir}/%{name}/test/* +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files premium-deps +%defattr(0644,root,root,0755) +%{_javadir}/%{name}-premium/*.jar +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files premium +%defattr(0644,root,root,0755) +%{_javadir}/%{name}-core-extras.jar +%{_javadir}/%{name}-server-extras.jar +%{_sysconfdir}/%{name}/management/commands-ext.properties +%{_sysconfdir}/%{name}/management/components-premium.xml +%{_libdir}/%{name}/agent/scripts/vm/hypervisor/xenserver/* +%{_libdir}/%{name}/agent/vms/systemvm-premium.zip +%{_datadir}/%{name}/setup/create-database-premium.sql +%{_datadir}/%{name}/setup/create-schema-premium.sql +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%files usage +%defattr(0644,root,root,0755) +%{_javadir}/%{name}-usage.jar +%attr(0755,root,root) %{_initrddir}/%{name}-usage +%attr(0755,root,root) %{_libexecdir}/usage-runner +%dir %attr(770,root,%{name}) %{_localstatedir}/log/%{name}/usage +%{_sysconfdir}/%{name}/usage/usage-components.xml +%config(noreplace) %{_sysconfdir}/%{name}/usage/log4j-%{name}_usage.xml +%config(noreplace) %attr(640,root,%{name}) %{_sysconfdir}/%{name}/usage/db.properties +%doc README +%doc INSTALL +%doc HACKING +%doc README.html +%doc debian/copyright + +%endif + +%changelog +* Mon May 3 2010 Manuel Amador (Rudd-O) 1.9.12 +- Bump version for RC4 release + +%changelog +* Fri Apr 30 2010 Manuel Amador (Rudd-O) 1.9.11 +- Rename to Cloud.com everywhere + +* Wed Apr 28 2010 Manuel Amador (Rudd-O) 1.9.10 +- FOSS release + +%changelog +* Mon Apr 05 2010 Manuel Amador (Rudd-O) 1.9.8 +- RC3 branched + +* Wed Feb 17 2010 Manuel Amador (Rudd-O) 1.9.7 +- First initial broken-up release + diff --git a/configure-info.in b/configure-info.in new file mode 100644 index 00000000000..ea9fd96a254 --- /dev/null +++ b/configure-info.in @@ -0,0 +1,3 @@ +These are the configure-time variables used to compile this CloudStack: + +@CONFIGUREVARS@ diff --git a/console-proxy/.classpath b/console-proxy/.classpath new file mode 100644 index 00000000000..0db855fac65 --- /dev/null +++ b/console-proxy/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/console-proxy/.project b/console-proxy/.project new file mode 100644 index 00000000000..414fed84978 --- /dev/null +++ b/console-proxy/.project @@ -0,0 +1,23 @@ + + + console-proxy + + + + + + org.eclipse.wst.jsdt.core.javascriptValidator + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/console-proxy/bindir/cloud-setup-console-proxy.in b/console-proxy/bindir/cloud-setup-console-proxy.in new file mode 100755 index 00000000000..9079c229ebb --- /dev/null +++ b/console-proxy/bindir/cloud-setup-console-proxy.in @@ -0,0 +1,187 @@ +#!/usr/bin/env python + +import sys, os, subprocess, errno, re + +# ---- This snippet of code adds the sources path and the waf configured PYTHONDIR to the Python path ---- +# ---- We do this so cloud_utils can be looked up in the following order: +# ---- 1) Sources directory +# ---- 2) waf configured PYTHONDIR +# ---- 3) System Python path +for pythonpath in ( + "@PYTHONDIR@", + os.path.join(os.path.dirname(__file__),os.path.pardir,os.path.pardir,"python","lib"), + ): + if os.path.isdir(pythonpath): sys.path.insert(0,pythonpath) +# ---- End snippet of code ---- +import cloud_utils + +E_GENERIC= 1 +E_NOKVM = 2 +E_NODEFROUTE = 3 +E_DHCP = 4 +E_NOPERSISTENTNET = 5 +E_NETRECONFIGFAILED = 6 +E_VIRTRECONFIGFAILED = 7 +E_FWRECONFIGFAILED = 8 +E_CPRECONFIGFAILED = 9 +E_CPFAILEDTOSTART = 10 +E_NOFQDN = 11 + + +def stderr(msgfmt,*args): + msgfmt += "\n" + if args: sys.stderr.write(msgfmt%args) + else: sys.stderr.write(msgfmt) + sys.stderr.flush() + +def bail(errno=E_GENERIC,message=None,*args): + if message: stderr(message,*args) + stderr("Cloud Console Proxy setup aborted") + sys.exit(errno) + + +#---------------- boilerplate for python 2.4 support + + +# CENTOS does not have this -- we have to put this here +try: + from subprocess import check_call + from subprocess import CalledProcessError +except ImportError: + def check_call(*popenargs, **kwargs): + import subprocess + retcode = subprocess.call(*popenargs, **kwargs) + cmd = kwargs.get("args") + if cmd is None: cmd = popenargs[0] + if retcode: raise CalledProcessError(retcode, cmd) + return retcode + + class CalledProcessError(Exception): + def __init__(self, returncode, cmd): + self.returncode = returncode ; self.cmd = cmd + def __str__(self): return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) + +# ------------ end boilerplate ------------------------- + +def check_hostname(): return check_call(["hostname",'--fqdn']) + +class Command: + def __init__(self,name,parent=None): + self.__name = name + self.__parent = parent + def __getattr__(self,name): + if name == "_print": name = "print" + return Command(name,self) + def __call__(self,*args): + cmd = self.__get_recursive_name() + list(args) + #print " ",cmd + popen = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + m = popen.communicate() + ret = popen.wait() + if ret: + e = CalledProcessError(ret,cmd) + e.stdout,e.stderr = m + raise e + class CommandOutput: + def __init__(self,stdout,stderr): + self.stdout = stdout + self.stderr = stderr + return CommandOutput(*m) + def __lt__(self,other): + cmd = self.__get_recursive_name() + #print " ",cmd,"<",other + popen = subprocess.Popen(cmd,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + m = popen.communicate(other) + ret = popen.wait() + if ret: + e = CalledProcessError(ret,cmd) + e.stdout,e.stderr = m + raise e + class CommandOutput: + def __init__(self,stdout,stderr): + self.stdout = stdout + self.stderr = stderr + return CommandOutput(*m) + + def __get_recursive_name(self,sep=None): + m = self + l = [] + while m is not None: + l.append(m.__name) + m = m.__parent + l.reverse() + if sep: return sep.join(l) + else: return l + def __str__(self): + return ''%self.__get_recursive_name(sep=" ") + + def __repr__(self): return self.__str__() + +ip = Command("ip") +service = Command("service") +chkconfig = Command("chkconfig") +ufw = Command("ufw") +iptables = Command("iptables") +augtool = Command("augtool") +ifconfig = Command("ifconfig") +uuidgen = Command("uuidgen") + +Fedora = os.path.exists("/etc/fedora-release") +CentOS = os.path.exists("/etc/centos-release") or ( os.path.exists("/etc/redhat-release") and not os.path.exists("/etc/fedora-release") ) + +#--------------- procedure starts here ------------ + +def main(): + + servicename = "@PACKAGE@-console-proxy" + + stderr("Welcome to the Cloud Console Proxy setup") + stderr("") + + try: + check_hostname() + stderr("The hostname of this machine is properly set up") + except CalledProcessError,e: + bail(E_NOFQDN,"This machine does not have an FQDN (fully-qualified domain name) for a hostname") + + stderr("Stopping the Cloud Console Proxy") + cloud_utils.stop_service(servicename) + stderr("Cloud Console Proxy stopped") + + ports = "8002".split() + if Fedora or CentOS: + try: + o = chkconfig("--list","iptables") + if ":on" in o.stdout and os.path.exists("/etc/sysconfig/iptables"): + stderr("Setting up firewall rules to permit traffic to Cloud services") + service.iptables.start() ; print o.stdout + o.stderr + for p in ports: iptables("-I","INPUT","1","-p","tcp","--dport",p,'-j','ACCEPT') + o = service.iptables.save() ; print o.stdout + o.stderr + except CalledProcessError,e: + print e.stdout+e.stderr + bail(E_FWRECONFIGFAILED,"Firewall rules could not be set") + else: + stderr("Setting up firewall rules to permit traffic to Cloud services") + try: + for p in ports: ufw.allow(p) + stderr("Rules set") + except CalledProcessError,e: + print e.stdout+e.stderr + bail(E_FWRECONFIGFAILED,"Firewall rules could not be set") + + stderr("We are going to enable ufw now. This may disrupt network connectivity and service availability. See the ufw documentation for information on how to manage ufw firewall policies.") + try: + o = ufw.enable < "y\n" ; print o.stdout + o.stderr + except CalledProcessError,e: + print e.stdout+e.stderr + bail(E_FWRECONFIGFAILED,"Firewall could not be enabled") + + cloud_utils.setup_consoleproxy_config("@CPSYSCONFDIR@/agent.properties") + stderr("Enabling and starting the Cloud Console Proxy") + cloud_utils.enable_service(servicename) + stderr("Cloud Console Proxy restarted") + +if __name__ == "__main__": + main() + +# FIXMES: 1) nullify networkmanager on ubuntu (asking the user first) and enable the networking service permanently diff --git a/console-proxy/conf.dom0/agent.properties.in b/console-proxy/conf.dom0/agent.properties.in new file mode 100644 index 00000000000..100f6532af8 --- /dev/null +++ b/console-proxy/conf.dom0/agent.properties.in @@ -0,0 +1,29 @@ +# Sample configuration file for VMOPS console proxy + +instance=ConsoleProxy +consoleproxy.httpListenPort=8002 + +#resource= the java class, which agent load to execute +resource=com.cloud.agent.resource.consoleproxy.ConsoleProxyResource + +#host= The IP address of management server +host=localhost + +#port = The port management server listening on, default is 8250 +port=8250 + +#pod= The pod, which agent belonged to +pod=default + +#zone= The zone, which agent belonged to +zone=default + +#private.network.device= the private nic device +# if this is commented, it is autodetected on service startup +# private.network.device=cloudbr0 + +#public.network.device= the public nic device +# if this is commented, it is autodetected on service startup +# public.network.device=cloudbr0 + +#guid= a GUID to identify the agent diff --git a/console-proxy/conf.dom0/consoleproxy.properties.in b/console-proxy/conf.dom0/consoleproxy.properties.in new file mode 100644 index 00000000000..743db37ce54 --- /dev/null +++ b/console-proxy/conf.dom0/consoleproxy.properties.in @@ -0,0 +1,6 @@ +consoleproxy.tcpListenPort=0 +consoleproxy.httpListenPort=80 +consoleproxy.httpCmdListenPort=8001 +consoleproxy.jarDir=./applet/ +consoleproxy.viewerLinger=180 +consoleproxy.reconnectMaxRetry=5 diff --git a/console-proxy/conf.dom0/log4j-cloud.xml.in b/console-proxy/conf.dom0/log4j-cloud.xml.in new file mode 100644 index 00000000000..bff32e3688c --- /dev/null +++ b/console-proxy/conf.dom0/log4j-cloud.xml.in @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/console-proxy/conf/agent.properties b/console-proxy/conf/agent.properties new file mode 100644 index 00000000000..137688c063c --- /dev/null +++ b/console-proxy/conf/agent.properties @@ -0,0 +1,2 @@ +instance=ConsoleProxy +resource=com.cloud.agent.resource.consoleproxy.ConsoleProxyResource diff --git a/console-proxy/conf/consoleproxy.properties b/console-proxy/conf/consoleproxy.properties new file mode 100644 index 00000000000..743db37ce54 --- /dev/null +++ b/console-proxy/conf/consoleproxy.properties @@ -0,0 +1,6 @@ +consoleproxy.tcpListenPort=0 +consoleproxy.httpListenPort=80 +consoleproxy.httpCmdListenPort=8001 +consoleproxy.jarDir=./applet/ +consoleproxy.viewerLinger=180 +consoleproxy.reconnectMaxRetry=5 diff --git a/console-proxy/conf/log4j-cloud.xml b/console-proxy/conf/log4j-cloud.xml new file mode 100644 index 00000000000..6910a5efc6b --- /dev/null +++ b/console-proxy/conf/log4j-cloud.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/console-proxy/css/ajaxviewer.css b/console-proxy/css/ajaxviewer.css new file mode 100644 index 00000000000..645f49ea19e --- /dev/null +++ b/console-proxy/css/ajaxviewer.css @@ -0,0 +1,86 @@ +body { + margin:0 0; + text-align: center; +} + +#main_panel { + clear:both; + margin: 0 auto; + text-align: left; +} + +.canvas_tile { + cursor:crosshair; +} + +#toolbar { + font:normal 12px 'Trebuchet MS','Arial'; + margin:0 auto; + text-align: left; + padding:0 0; + height:32px; + background-image:url(/resource/images/back.gif); + background-repeat:repeat-x; +} + +#toolbar ul { + margin:0 0; + padding:0 10px 0 10px; + float:left; + display:block; + line-height:32px; + list-style:none; +} + +#toolbar li { + float:left; + display:inline; + padding:0; + height:32px; +} + +#toolbar a { + color:white; + float:left; + display:block; + padding:0 3px 0 3px; + text-decoration:none; + line-height:32px; +} + +#toolbar a span { + display:block; + float:none; + padding:0 10px 0 7px; +} + +#toolbar a span img { + border:none; + margin:8px 4px 0 0; +} + +#toolbar a:hover { + background: url(/resource/images/left.png) no-repeat left center; +} + +#toolbar a:hover span { + background:url(/resource/images/right.png) no-repeat right center; +} + +span.dark { + margin-right:20px; + float:right; + display:block; + width:32px; + height:30px; + background:url(/resource/images/gray-green.png) no-repeat center center; +} + +span.bright { + margin-right:20px; + float:right; + display:block; + width:32px; + height:30px; + background:url(/resource/images/bright-green.png) no-repeat center center; +} diff --git a/console-proxy/css/logger.css b/console-proxy/css/logger.css new file mode 100644 index 00000000000..2ecabfd35bf --- /dev/null +++ b/console-proxy/css/logger.css @@ -0,0 +1,120 @@ +@charset "UTF-8"; +.logwin { + position: absolute; + + z-index:2147483648; + width: 800px; + border: 1px solid gray; + background: white; + text-align: left; +} + +.logwin_title{ + width:auto; + height: 23px; + background:url(../images/grid_headerbg.gif) repeat-x top left; + border: 1px sold #737373; +} + +.logwin_title_actionbox{ + width:175px; + height:16px; + float:left; + margin:4px 0 0 7px; + display:inline; +} + + +.logwin_title_actionbox .select { + background: #424242; + font: normal 10px Arial, Helvetica, sans-serif; + float:left; + border: 1px solid #6e6e6e; + height: 16px; + width: 100px; + margin-left:3px; + padding:0 0 0 3px; + color:#CCC; +} + +.logwin_title_rgtactionbox{ + width:49px; + height:15px; + float:right; + margin:4px 0 0 7px; + display:inline; +} + + +a.logwin_playbutton { + width:18px; + height:15px; + float:left; + background:url(../images/play_button.gif) no-repeat top left; + margin-right:2px; + padding:0; +} + +a:hover.logwin_playbutton { + background:url(../images/play_button_hover.gif) no-repeat top left; +} + +a.logwin_stopbutton { + width:18px; + height:15px; + float:left; + background:url(../images/stop_button.gif) no-repeat top left; + margin-right:2px; + padding:0; +} + +a:hover.logwin_stopbutton { + background:url(../images/stop_button_hover.gif) no-repeat top left; +} + +a.logwin_clrbutton { + width:28px; + height:15px; + float:left; + background:url(../images/clr_button.gif) no-repeat top left; + margin:0; + padding:0; +} + +a:hover.logwin_clrbutton { + background:url(../images/clr_button_hover.gif) no-repeat top left; +} + +a.logwin_shrinkbutton { + width:18px; + height:15px; + float:right; + background:url(../images/shrink_button.gif) no-repeat top left; + margin-right:7px; + margin-top:4px; + padding:0; +} + +a:hover.logwin_shrinkbutton { + background:url(../images/shrink_button_hover.gif) no-repeat top left; +} + +a.logwin_minimizebutton { + width:18px; + height:15px; + float:left; + background:url(../images/minimize_button.gif) no-repeat top left; + margin-right:2px; + padding:0; +} + +a:hover.logwin_minimizebutton { + background:url(../images/minimize_button_hover.gif) no-repeat top left; +} + +.logwin_content { + overflow:scroll; + height: 477px; + background: white; +} + diff --git a/console-proxy/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-console-proxy.in b/console-proxy/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-console-proxy.in new file mode 100644 index 00000000000..488df568f02 --- /dev/null +++ b/console-proxy/distro/centos/SYSCONFDIR/rc.d/init.d/cloud-console-proxy.in @@ -0,0 +1,81 @@ +#!/bin/bash + +# chkconfig: 35 99 10 +# description: Cloud Console Proxy + +# WARNING: if this script is changed, then all other initscripts MUST BE changed to match it as well + +. /etc/rc.d/init.d/functions + +whatami=cloud-console-proxy + +# set environment variables + +SHORTNAME="$whatami" +PIDFILE=@PIDDIR@/"$whatami".pid +LOCKFILE=@LOCKDIR@/"$SHORTNAME" +LOGFILE=@CPLOG@ +PROGNAME="Cloud Console Proxy" + +unset OPTIONS +[ -r @SYSCONFDIR@/sysconfig/"$SHORTNAME" ] && source @SYSCONFDIR@/sysconfig/"$SHORTNAME" +DAEMONIZE=@BINDIR@/@PACKAGE@-daemonize +PROG=@LIBEXECDIR@/console-proxy-runner + +start() { + echo -n $"Starting $PROGNAME: " + if hostname --fqdn >/dev/null 2>&1 ; then + daemon --check=$SHORTNAME --pidfile=${PIDFILE} "$DAEMONIZE" \ + -n "$SHORTNAME" -p "$PIDFILE" -l "$LOGFILE" "$PROG" $OPTIONS + RETVAL=$? + echo + else + failure + echo + echo The host name does not resolve properly to an IP address. Cannot start "$PROGNAME". > /dev/stderr + RETVAL=9 + fi + [ $RETVAL = 0 ] && touch ${LOCKFILE} + return $RETVAL +} + +stop() { + echo -n $"Stopping $PROGNAME: " + killproc -p ${PIDFILE} $SHORTNAME # -d 10 $SHORTNAME + RETVAL=$? + echo + [ $RETVAL = 0 ] && rm -f ${LOCKFILE} ${PIDFILE} +} + + +# See how we were called. +case "$1" in + start) + start + ;; + stop) + stop + ;; + status) + status -p ${PIDFILE} $SHORTNAME + RETVAL=$? + ;; + restart) + stop + sleep 3 + start + ;; + condrestart) + if status -p ${PIDFILE} $SHORTNAME >&/dev/null; then + stop + sleep 3 + start + fi + ;; + *) + echo $"Usage: $whatami {start|stop|restart|condrestart|status|help}" + RETVAL=3 +esac + +exit $RETVAL + diff --git a/console-proxy/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-console-proxy.in b/console-proxy/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-console-proxy.in new file mode 100644 index 00000000000..488df568f02 --- /dev/null +++ b/console-proxy/distro/fedora/SYSCONFDIR/rc.d/init.d/cloud-console-proxy.in @@ -0,0 +1,81 @@ +#!/bin/bash + +# chkconfig: 35 99 10 +# description: Cloud Console Proxy + +# WARNING: if this script is changed, then all other initscripts MUST BE changed to match it as well + +. /etc/rc.d/init.d/functions + +whatami=cloud-console-proxy + +# set environment variables + +SHORTNAME="$whatami" +PIDFILE=@PIDDIR@/"$whatami".pid +LOCKFILE=@LOCKDIR@/"$SHORTNAME" +LOGFILE=@CPLOG@ +PROGNAME="Cloud Console Proxy" + +unset OPTIONS +[ -r @SYSCONFDIR@/sysconfig/"$SHORTNAME" ] && source @SYSCONFDIR@/sysconfig/"$SHORTNAME" +DAEMONIZE=@BINDIR@/@PACKAGE@-daemonize +PROG=@LIBEXECDIR@/console-proxy-runner + +start() { + echo -n $"Starting $PROGNAME: " + if hostname --fqdn >/dev/null 2>&1 ; then + daemon --check=$SHORTNAME --pidfile=${PIDFILE} "$DAEMONIZE" \ + -n "$SHORTNAME" -p "$PIDFILE" -l "$LOGFILE" "$PROG" $OPTIONS + RETVAL=$? + echo + else + failure + echo + echo The host name does not resolve properly to an IP address. Cannot start "$PROGNAME". > /dev/stderr + RETVAL=9 + fi + [ $RETVAL = 0 ] && touch ${LOCKFILE} + return $RETVAL +} + +stop() { + echo -n $"Stopping $PROGNAME: " + killproc -p ${PIDFILE} $SHORTNAME # -d 10 $SHORTNAME + RETVAL=$? + echo + [ $RETVAL = 0 ] && rm -f ${LOCKFILE} ${PIDFILE} +} + + +# See how we were called. +case "$1" in + start) + start + ;; + stop) + stop + ;; + status) + status -p ${PIDFILE} $SHORTNAME + RETVAL=$? + ;; + restart) + stop + sleep 3 + start + ;; + condrestart) + if status -p ${PIDFILE} $SHORTNAME >&/dev/null; then + stop + sleep 3 + start + fi + ;; + *) + echo $"Usage: $whatami {start|stop|restart|condrestart|status|help}" + RETVAL=3 +esac + +exit $RETVAL + diff --git a/console-proxy/distro/ubuntu/SYSCONFDIR/init.d/cloud-console-proxy.in b/console-proxy/distro/ubuntu/SYSCONFDIR/init.d/cloud-console-proxy.in new file mode 100755 index 00000000000..eee7c454d9a --- /dev/null +++ b/console-proxy/distro/ubuntu/SYSCONFDIR/init.d/cloud-console-proxy.in @@ -0,0 +1,95 @@ +#!/bin/bash + +# chkconfig: 35 99 10 +# description: Cloud Console Proxy + +# WARNING: if this script is changed, then all other initscripts MUST BE changed to match it as well + +. /lib/lsb/init-functions +. /etc/default/rcS + +whatami=cloud-console-proxy + +# set environment variables + +SHORTNAME="$whatami" +PIDFILE=@PIDDIR@/"$whatami".pid +LOCKFILE=@LOCKDIR@/"$SHORTNAME" +LOGFILE=@CPLOG@ +PROGNAME="Cloud Console Proxy" + +unset OPTIONS +[ -r @SYSCONFDIR@/default/"$SHORTNAME" ] && source @SYSCONFDIR@/default/"$SHORTNAME" +DAEMONIZE=@BINDIR@/@PACKAGE@-daemonize +PROG=@LIBEXECDIR@/console-proxy-runner + +start() { + log_daemon_msg $"Starting $PROGNAME" "$SHORTNAME" + if [ -s "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") >/dev/null 2>&1; then + log_progress_msg "apparently already running" + log_end_msg 0 + exit 0 + fi + if hostname --fqdn >/dev/null 2>&1 ; then + true + else + log_failure_msg "The host name does not resolve properly to an IP address. Cannot start $PROGNAME" + log_end_msg 1 + exit 1 + fi + + if start-stop-daemon --start --quiet \ + --pidfile "$PIDFILE" \ + --exec "$DAEMONIZE" -- -n "$SHORTNAME" -p "$PIDFILE" -l "$LOGFILE" "$PROG" $OPTIONS + RETVAL=$? + then + rc=0 + sleep 1 + if ! kill -0 $(cat "$PIDFILE") >/dev/null 2>&1; then + log_failure_msg "$PROG failed to start" + rc=1 + fi + else + rc=1 + fi + + if [ $rc -eq 0 ]; then + log_end_msg 0 + else + log_end_msg 1 + rm -f "$PIDFILE" + fi +} + +stop() { + echo -n $"Stopping $PROGNAME" "$SHORTNAME" + start-stop-daemon --stop --quiet --oknodo --pidfile "$PIDFILE" + log_end_msg $? + rm -f "$PIDFILE" +} + + +# See how we were called. +case "$1" in + start) + start + ;; + stop) + stop + ;; + status) + status_of_proc -p "$PIDFILE" "$PROG" "$SHORTNAME" + RETVAL=$? + ;; + restart) + stop + sleep 3 + start + ;; + *) + echo $"Usage: $whatami {start|stop|restart|status|help}" + RETVAL=3 +esac + +exit $RETVAL + diff --git a/console-proxy/images/back.gif b/console-proxy/images/back.gif new file mode 100644 index 0000000000000000000000000000000000000000..5c61ae27719fd2cccd372b6c95a19328812feb46 GIT binary patch literal 149 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 LSE)(}FjxZsXaKX; literal 0 HcmV?d00001 diff --git a/console-proxy/images/cannotconnect.jpg b/console-proxy/images/cannotconnect.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0599f17beb25987e999230a1429047408e85284a GIT binary patch literal 1810 zcmbW%c{tR07y$6^Z|<4IxJCxK8b+yg4MS~`qb6iiIg>P2GflCq5S4Z27{qL)Hp!JM z%9SkZN^Uxckeh^cWOCQcj@^CQ=h?q@d%w^7{_}mG@AH11@AJI;H~b-hKV(KT0}up( zQ-TBd!(h9qAJYv08f`nk001BW!I6Rr08x+|aW5PKaRq|jK41(`2qX%LK%tN*G#Z7$ z;c+-D7AGMrB7~Qc*s?`RfrUo%CZE4g1UmTimKXnHEH>s8av4vB(fTL;}Hmr zM&mFzaU4#ZOe7G=|2z2AKokSafh!zR0x(esE(-DMfV^OMq(HWT`#T^QTre*hgT-wU zG?d^041&X92sjdnKnS`o3+@3z6e*^tX@n9#;(}K4l+X%_Pr)efFQ}2UemTEQ`?Oau z7DtfUB26TzsFKyT>riy{sQL!RCZ=Ww4$=-CJ$Bs2*3RC6?)tsk83yz0kKPx2eElx^ zhlGZOUyX>2O5pr_{l?A2Tenlw(lhR7W@YCT78RE~E`3t=YwgpzXY~z@P0g=5I$w8n zzv<~69vK}Qe>d@da_YkZ_v7Ny^2+Mkh6@7lzgS;n|8j{6TrdOzjzDj?AegTp;i3qn zq9#hr=m^@yQ(Q?a2qUpSKBb@rtE_E3FL~PQC62I7XPES1gZ8KFzrljPO7;czkE<65 z!6Cut!9{@~ShbEK--CTc3Ug}w zRO&Oz1074}qes%6k_gXEaaGT|jkYf&>T2W(1*EszhQ%qXD&rh(4a?(qg;74)+9bPo zP-N)Li)TriQkFGAwl%#|%@42$&qjrKN1p75$DH9dvsp_S)6NHZ6UFO#$*8=-&oZsT zPVRg1aVtyZ=f5C8Vj)?my(_F)=xp20PeSk@D3 zqzCHTr!s2ITC2m_b%P;k_~vLEKEP3I$`S^aZC|RZ@H#JVC6(Q_c>Y5Q23x4RYG*JU zi_X{5Mn_jFB#X+6-RQVY=yF%j$Fewz(Z7EvJe5%9)mOw})2kdxaxYo%UcCO@qpI*y zRS_%Gp=asNY;$d;jgKcUb1tx?Bi7Vyae)u|Joun=^Xf(3;lUVopU74Jq)x&#X*P+K z-Jf1)Kgp>`W|Y~rpmvntb{(QZ{pONGvh30kkudMM%}-M*lOFU9mMn(qS=R?UHMe5b z^-Mxs8sMZS`jXl5b*!VF^z0K1k6q>Wu?e-KGl@Zx{r#63D=Nwr;~yE{qn-!}xy!0s z`&?O``99)YMlc^(6`8B4tnXut$7nx_Sc2t+G`+1}f~DEq4B!#@pt@@4Tm6c>l}SE8 z4MyfZWDLm8&lD4V?o(JX?kvpgz{mjO)g~s1O_McK+5hU4mb;76sAu)=D-73p;gqev zsMqs>r2$*lJgX%G&6&~cyMvz?Ti{LaxGrp`)+CFU%Ua2$b*az1>nc{uFKG#&6gg&8 zY1WWUdSsdiwXIVrW;Ukivtt+9%9FSDwjq{TX$C9d6P8Z90_w~Ap$MgVCzm(4M*4T@ zb30n_Y05;VZ{T#`iqtf>(>!x@#d)#d^!o7L-1FITdv`jUlvl(Z39%@P8em%Wq$kG9 z=xhFTY&0n&;{Coo$L;q@PpPns&g8uBe!S4;iCDJb(#GwJF&-<7jEv2I!$uMqE`##T^~wjC!|bEh=pd`Hgf zdI(J!Y;(}*S}Hsl-6t+LYat}=BIa)VZrseThwJ*WHfZpA-&*&l@KWb)E3Uo)IW;l( NE7r!wsuAJ$`~kw83zz@^ literal 0 HcmV?d00001 diff --git a/console-proxy/images/clr_button.gif b/console-proxy/images/clr_button.gif new file mode 100644 index 0000000000000000000000000000000000000000..f5c385829da0dda9ec5a86372aa2fb7a57c85a69 GIT binary patch literal 1274 zcmVkP*Z=AOLmD$XmxLUfJjwZ zYI%Qhlc86Dl52H&KSD!kdxL9hYfOfodaJ)ndW~*!zU`S6=Q-G6ScY$wuepFjtQ+``WiK)21V}FZeWMoQcbcC3o)!EuWMoDF5Wx31JTX=939w!FV>euPwNb4^Z8RCk7JZf{g!XI*G*ReFkNYi)+3uV!y^ zNnK^P!pBQ>he%FQf}pIs#mSAOuWERHUxbv^*xGDzbV_f2Yjt{HZgj)X)Oe7YM@UGy z#>-r8c1dJ!LPSJsba+xyQd@U}Sd^`TpQ~wlfUUT_dzYiPz{JDO(}9zlRe6Y4e2Z*v zaDAMpewCd@M@Mpzoob1d)YsT@m858XhmWSPS95-Hi>>KU2Ab*Y;j6!c}i`2l(D^2e2&D<({6l%Szu#$n5BQ4 zrf7hQYlM$nbbe1zQD1d_Uv_^`P*G=WZ@I+Dn5(l`ldM;Qlxuc+y2Z*=WNVL~rEiOt zSAmjPXm4eAep6FZW^iqOdyG|okyn9}NK{x!Vr)Q2OHNKsPESwD z(9%nEhRxB_A^8LV00000EC2ui02}}h000R80A0Wc*I)nw007Vcn1Ue5fB_&5c4?un z1povCQuz`Xz*`y#D0FCeqCv+Q3N<9upm64i2@}^eP>`Tz&ptE!OgZ|%Aw;|;V?uCP zbnXKrWS(4Yld-J{hfHMp1R(N49K0VutZWg3r5TWUd49Na#!Xo#M0QrOnKdX-fB-LO zMfw)zAB4NLj1e0JE=MqJTzZtT)@a?XXv~}hNcP84oqw-busCrASRz!L!2CMZgA<){ zxyW^c=0-pOUBaHly2CEWJ#!vBpaa#bRFF54NRU_~N)fDf*i7&6qi52%3p_61xX`9j zwrV_Zz=`pNR}5mVm@fXG3i515>V8F1VRh1DVstAgc&%q(n2M8=# z7K#cfKr_q`g%Ro*GK4Bdlu!aA{m4_m1{MgALIp)gfrJX5D4+lpeBe@uh6)HFix7`Y zBaJ$E*g${*0uUj`0TVb-fDe~M07V25WT2xKP|)LpEl3owfCB{FP=OPgG{A%bG)Q5? z5(`9m1R(`fdBiOaJiq_}1khl?0ZastMG6flalj1|5Wv8e1mN?)23k%~garoFc|Zk9 kU^0gYUcA$Q01+iHMFNB(WM~2oJv4v-0#H(n2mt{AJI!|_qW}N^ literal 0 HcmV?d00001 diff --git a/console-proxy/images/clr_button_hover.gif b/console-proxy/images/clr_button_hover.gif new file mode 100644 index 0000000000000000000000000000000000000000..f882fa0baef700d492cd02fe95a274ab7962e5a7 GIT binary patch literal 437 zcmZ?wbhEHblwsg!xXQrb?(S}HZ=ai+TU1mO7Z(>25|WXT5g8eol$5k>+qS^K!1ng` z{QUfT_wFrPw8+`n+0D(Zrl#iT(W8}>m3#N@ZEkKpeE4v5bhML`Q$az&&Ye3O8XC@? zJ)51KJ!{r1e}Dfyd-hzrcFoVv@9Nd7Yu2nOE-tRFu1-x&4GId%$;t8c^=)l!y>jKs znKNf*%$N}n5HMxRl!+53_VxAc-o1PE>eX3US;4`)Ucg&Ru`i;Q89oC%A9bg#5RFP~hm8~c1KvH6SH zjRhAAiU|s8iy19;W8EYywP_oxl<+oTWhq@{gY9k%N5plGFmUNHaPpks;XJ}AdV=Bb z2~IvPE!3T`{ Z8WDw0sv;Efe8Qr literal 0 HcmV?d00001 diff --git a/console-proxy/images/gray-green.png b/console-proxy/images/gray-green.png new file mode 100644 index 0000000000000000000000000000000000000000..e785a63f83eca4d54e2ead7ee4035b1ce4f508ed GIT binary patch literal 3833 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 H3=Gx)?!i0) literal 0 HcmV?d00001 diff --git a/console-proxy/images/left.png b/console-proxy/images/left.png new file mode 100644 index 0000000000000000000000000000000000000000..fb7506618d7c3db40d02c31c3fdad17f5c6b3bd0 GIT binary patch literal 3024 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 Sd=wA>0000OZy#r8XODmo zcQ0>uU;ntcIFGRC?Ck9H+yW;jr;Ln@kkGJ*tiq(EWDozKjEqc=kVucvsECwI58r^u zm^eQ_zmSj+e}DgstgOhWsEERv$b{s?^vv{}e9!odxVZSp$jI2VOpoA*kcg<* z+_;2955EA{PLLGVs{_k#GHbJ+~V|{T#v|v zh`jQoloZdD{P3dMn1V_VzrdiNpoF~Q)YQ~~fB?_f)RdHzz`(%Z;9!P92^4>_fZe47 zB0+J&z<#}fozIhtp{>1xfla=NK|z>#;-tyU>}+xha!j-5%$+O1AjB>#p=fTWCb&w? z&RkJKxQ$)R(L`0x#@5}|Mo-nmQLK$mLtj_R!phRc(#k?hS6`z|Q%P1uTGT<=S=m8U zT18f=O>(V|7DGaUp>j?hTI3to(IX0)ieNZ)W9{ z;9+s#cVyz@4$G)mka(1dk5hnWO2PuAW?9|@k;aP&?93Ax86*M)7A!kE+k#Ptalrx= Yzd39w28|6%JQuojGA(Hk;9#%@0IxicasU7T literal 0 HcmV?d00001 diff --git a/console-proxy/images/minimize_button_hover.gif b/console-proxy/images/minimize_button_hover.gif new file mode 100644 index 0000000000000000000000000000000000000000..7837ed00baf57a8978cf13cafcc71b456a1f7b30 GIT binary patch literal 227 zcmZ?wbhEHb6k_0KIKsf-?(S}HZ=aNu6c-m485x<8k&&C5TU1mO5)yLn-aR)rH)m(( z?CfkOCnrBYKYxG!prD|@z`)eh)RdHz;NakZfB*)(K=CIFSY8K2g6w2qO;}(M;+-%r zV}U}_2N8Fj^X&^P`jg+(+&6A)Ti~E0)vwlAbTWs_r+Gz*m+TCV8*9qE4HWm?Z>gBW vpzPDIrRpr(g4-XySl{E*dQp0U={%ng6L)z97q1|Xkcg_LCIbtPB7-#mJBdP0 literal 0 HcmV?d00001 diff --git a/console-proxy/images/notready.jpg b/console-proxy/images/notready.jpg new file mode 100644 index 0000000000000000000000000000000000000000..406599cd595865c8d4dcd4ec79a0cefc370bcaeb GIT binary patch literal 1827 zcmbV~c{mh!7{`Bek1@jxGeg=E8dsJ|NkUIThRxC8DoT!`hLDV-7*UR7-8nYpm>ka} zAA`6*kRmt0oLP+I`yR*}r!C{@(BNyuau7zVGvXp69ppZ0R+?+uKrY z0R#d8hh+yW34nPx_j^bgYKUG zfx)5Sk!R2O0^#_t6K~$Wo17B={_&65PoE`U=9al20QMKwH?n_m5oBCYI2;B?EptJj zkut*waD>JNq|y!-luxL#=0*mZXq9=bu0u}Cc#nv5BGav_>W5o;iedae5q%Sb^ z(AylNOzA7UlTpJx_E;s>zJGBe{jDH4Bc^Jy1pD4$)Po+}PtLbHXM<(o-Zk-dB@}6I zF&*=H#nOw(Y7_v?dW2#4UZCc_CKs(+*>|JNQSGtDyT#cPPX#60OTq1njnQ?x1xVXM^PW_xf#2R4)4BR*+joQ49lft}o zy!mSLC4ql#>4uDk^Sd)^DpPivUkDA!4KWWDT3DSq62pqy*HYxM-^-C&>vy^h(=(+* z;nHE6>t7a=P?bb)`m3(!i*sEg(Ti0PCbz2D#o69=?_){1gN_boMM!HOj;X$)vmVdU z`wWd+G%zzN))*W1S^~K3t7a`Lq-Bf5g%y(6HY1VOrE7xsT6g&hHAj6bEEpqxgExeV z6EE7zJ~g}ck!?MA2T0CtK_6U^oRB28ThijnvwZRuOuwZfzx=GKSni2Xy!qh_^P|&z zO2WgFZS=H9#n%28HSaNgI@g;$;U}4Plk|#VU90g;B4*WiiBga^xyf$)f_3ZA4CPje zR(rZDGA!N@`2?k^TIMY29hZ9IpesN8Aj$fquGswp+dNY3PFEU}4G64`xEHyYX1$9v z)k!KXU~c-YNIG2qG~|r?tE|b-A)m*l-5MJ#FH1CDf561<7j3!V$Ps)w(P|MRl%9Af zxTE9VmfE9%Ow@Lw1QdE@p`2rf$L=Q`c>vwF8ZM!V=7l}6Mq=yLiUYC62x(J_EvxiK zT{tq^oK3sgd^;-GFxb;wE8M`Pr_*2cU~PWZ@i#Txv@2%ZK?=7;d_6hXsG>P*#-Sj> zgYQ;o*IGa^I7H#E(F&MyrkRI&>2#=HYFYy5R)@ft@KuX-Oi%vUD4#zXv^&tUfYN8R zg;SSPo<<8ZVNwW9FDHxf!}U2=QYu2U%X;H1B1Ycep4%ACECzOS^Q7BWsy9s3CN!{o zJEKDq8;r2;*okHad+ChWbu}-h@_d~$FE>y_DMOv6c3p?2isK(mdGsu_8F|-=KabZh z&`Nj=Rzz4Kr97{@N>b!$_NeakYS*9B(s;xJPvr?}=zHx>I$LXXE7NB~I16KAUL4I| W8XRsdB&6zAuhlJ01>fVP=YIlBFcT{P literal 0 HcmV?d00001 diff --git a/console-proxy/images/play_button.gif b/console-proxy/images/play_button.gif new file mode 100644 index 0000000000000000000000000000000000000000..6f483082ecb33d34bb09dd744ff77521d6a54aa3 GIT binary patch literal 657 zcmZ?wbhEHb6k_0Kc*ekBZ*TAJ?w*vC6c-m~@8A#;5_0d}y~xPOjEoF7H#c`LZ)a!c z+}vDu4-a>5A9qjBqN3uWq9P|JC$H4P?CflhfMCz4q};rGkDzc5-vB>9KM%ivqT&*N zfB&4q;&Yd;rWKaD`}h?V7iVN;c|^o}#-_S^dZp(TCT8UY1O%iNR|E$KM`RcK6gGNB zCb)b1=H}-4p=j5iQrbbt_$0n!6C8c;KW_$PthZNL=*L3GK_atWKgoK6X6cpv;7p&WR*fTkI z)z)1eA(8RrjY%nK=dN5!E3S+$sR@Zqj4!T^NYC}kt&S(@*$mi#;!hT^unvd>#R&uZy9Op^O>Ty^_6`P4)+Pp74nYAW z8G9Ke0RcflCQd0?DIT6AaZ@=5IbT0iVsilrZ`TaK5cexcFW^8QocIgp5 z@7fsuN~#D~T&Na0>?gtc_s3Um$HOPYm{X#BHe5LB;>7TmY0rTKMz1Lr!d_=MJ}NA1 zVfibPGr_URms32bPa|VOz`3S6ewmyV4UE36JbW2ybC?qxo7nh8EF>N~G;%KFR9Ui6 WA;ndZDdKv9YJtW9My^H%25SKAOWz&< literal 0 HcmV?d00001 diff --git a/console-proxy/images/play_button_hover.gif b/console-proxy/images/play_button_hover.gif new file mode 100644 index 0000000000000000000000000000000000000000..6689e3eec8145fca0cb0179380228b77bd320bf1 GIT binary patch literal 243 zcmZ?wbhEHb6k_0KIKsf-?(S}HZ=aNul#!7U7Z;bCn;RJ!84?mwR8(~D-aR)rH&0Jb zXJ_Z^>})3|r*r4d`TP6(`T3=$rUnKE1_uWR1qCH0CKeVJ1_T7GTD5BB%9U%@tjWyG zOi4*OfBrlJ1B2pE79h#MpaUX6b~3OQEO1EhNtljAhIHOa^h)I6}dU@u9axa=I>npw8nt(UV%f^<__jNWtGeF qj^8$|spaFUY^)XKsqD&Ulx5^;>CWfk6Xq2XQPtFBVB=L}um%8Cp;dVR literal 0 HcmV?d00001 diff --git a/console-proxy/images/right.png b/console-proxy/images/right.png new file mode 100644 index 0000000000000000000000000000000000000000..4abac818410842f8fdb83eced02d0034451cab7d GIT binary patch literal 3131 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 literal 0 HcmV?d00001 diff --git a/console-proxy/images/winlog.png b/console-proxy/images/winlog.png new file mode 100644 index 0000000000000000000000000000000000000000..c7668c52f532f703f9dd8e4655fdb31dac58a9d5 GIT binary patch literal 2629 zcmV-L3cB@)P) zc&5)Z@6<^gI}azWy?*V>`41%AK}MmpJ<-8*x>`ObH~a~)=kq5t;5 z>3`s`l7D9I8hGz7j7WqoacRgJR$fTa*rhpn8&7#7N@@|sX4&=ln@-9(R4iEWsTD;p zITnq5^JcPSnpRfYRIb)YhD7rVHHu?JMiW(Q0lt&e3L@Hj-x#eg#28hg;9E;?Hy$ISgyk~w zeLl; z+YnCnM2fZ@IxzEtE(l}2tD3kmmg2+QlD?O*$mtX0X2C!o7yu(Ioq)hPkv!Z`6#2o@ za`EW(x6oH4O%E>CX1^m;;qy99{n1G4geE;NwRWU5!;OyI-U#HKejzpe1PEgr?xVPl8Qi0kFD z)-KmhkxbU0=E^9N20p8OazTTe=*u>E9#IB$K6hs6Wu&XGg4mF0uY%|>>mIJ0v8Ik^ zE>u#-E=L+D%3MeZDg?esO1i_S`<_-s&(7qCK*=zATFIt^L`ID^MdxxZoE6 zz(6cQTzlkLEplwU46=DCFdK@}()7ylr1GmE9?N~P3qUhLACcpe6SJ4Te$c;RIv(ET z6vl2J=o=dFip8vb%~0|33Rk}yp1tthYsP>1g}(NI&sm?ne?U#E-Gllxixsj1a)~y>D z@$#O?<$TKLJn}uXOUA1Po3Gv9*t_qE{NnV}ZFgz7J~?xz?_zL?7|M^4Ou+VeHw5dQ zCoEXDCdc(@dR86*V@u3&yXMseaYMG#y+Ap))usJBWi}SAlns@GLiCi-a7$&8D`WB_ z>gNshaFFsoQ{H&pL+s?wfAiWK)gR3-o_>7i_Wr&?-X$BNw4ft_Y6;l8?P%hG22-4Z zvYKy|)T^%VhD;>-VLcG*`yqF9Tcs7>RhLxMn7>9PUT71#>`E92jk=Kxx6*PW%VIUO zGs_=*`r*f4d42NY{#TEme#m%@PdILCV>;Ebf;h&yNX{>%mn)_I$;PaERF>p{uwFfu z>GONT~t>;$WIZ)euM2IEw9c%3HI(MYLHiA-d1RMN4Wx9`M7<)C!lbmvcQd)Xo5v?9 zl`lh6Rnj#WcE`}$?UA`-7uW=2va5wPAZ&nupWh#}VyFo}f&mkOflyG>P5{JM1GRJ3 z-Tnw8>V?6@<0hK^hRN(87B2!=fSJ=ktn|*iwvFeuEz%8iMQqVjrEh9sIheGsbPu~) z6RsniR$bo{v?H?76$)%HSghn5Qq5OLx;=rG+v zptQP<(NiM=L#Bnn(TZ_zir$*@4%Hhg%RXFJV{!*U`yCLL5StqH@Fv;r(QV*(#^PN@ zVlc7-2B4HKh-Rc|L!bC7RsX!rS}%u{oCmrXZx0}*-37r-1FEi*X(D+;p*>@&vEk_C zWjgFw3}ejDsa7kKueVN3998wXslIfkp3CKQHeV=o`ZnKQ%8fo?53IX(ET?B}er_)7mF{FH z15bK6Q3?hY$%xP;D1q*lK{xSRBSQN-bZW^zF?C_#eChJkZHYf3l6~m-Du{)JGe4gC z^$&lhn~M{8S`|hYS;Z`6#d;d_1}N<{o@><>HqD2dA6LN*nQAN!5w)|o1gl;apGtRqy>tGiI zWn=TD5RTB7M%m)mSf!OR>lePH=9V39Cp%rNkv$>|1Wh4;bnuB_0O4VF%v#0zo8#pp zL8F$xGQLZQq#n6d5J@}zvm@#2NZas-xwcL1XMXyJ&3fnX;v+k*X>844pOEb0RG0wK zm^F#T8)`$`=T*A=i`T*z#wV*Y=*Skn{$P^$3XrMQa=hQ8yS=wV@~pUhq_X9SmuE+R zHF{?8mY@Y&Pz?u(7kjI8tP#W+&@LYddfjkwZi1?6@SbxiUX5=5il n*~`Xj|HAnHe+~4GzX>n^g*+DN(p{zl00000NkvXXu0mjf;#L?^ literal 0 HcmV?d00001 diff --git a/console-proxy/js/ajaxviewer.js b/console-proxy/js/ajaxviewer.js new file mode 100644 index 00000000000..ad13c99b186 --- /dev/null +++ b/console-proxy/js/ajaxviewer.js @@ -0,0 +1,917 @@ + /** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// +// AJAX console viewer +// Author +// Kelven Yang +// 11/18/2009 +// +var g_logger; + +function StringBuilder(initStr) { + this.strings = new Array(""); + this.append(initStr); +} + +StringBuilder.prototype = { + append : function (str) { + if (str) { + this.strings.push(str); + } + return this; + }, + + clear : function() { + this.strings.length = 1; + return this; + }, + + toString: function() { + return this.strings.join(""); + } +}; + +function AjaxViewer(panelId, imageUrl, updateUrl, tileMap, width, height, tileWidth, tileHeight, rawKeyboard) { + // logging is disabled by default so that it won't have negative impact on performance + // however, a back door key-sequence can trigger to open the logger window, it is designed to help + // trouble-shooting + g_logger = new Logger(); + g_logger.enable(false); + + var ajaxViewer = this; + this.rawKeyboard = rawKeyboard; + this.imageLoaded = false; + this.fullImage = true; + this.imgUrl = imageUrl; + this.img = new Image(); + $(this.img).attr('src', imageUrl).load(function() { + ajaxViewer.imageLoaded = true; + }); + + this.updateUrl = updateUrl; + this.tileMap = tileMap; + this.dirty = true; + this.width = width; + this.height = height; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + + this.timer = 0; + this.eventQueue = []; + this.sendingEventInProgress = false; + + this.lastClickEvent = { x: 0, y: 0, button: 0, modifiers: 0, time: new Date().getTime() }; + + if(window.onStatusNotify == undefined) + window.onStatusNotify = function(status) {}; + + this.panel = this.generateCanvas(panelId, width, height, tileWidth, tileHeight); + + this.setupKeyCodeTranslationTable(); +} + +AjaxViewer.prototype = { + // client event types + MOUSE_MOVE: 1, + MOUSE_DOWN: 2, + MOUSE_UP: 3, + KEY_PRESS: 4, + KEY_DOWN: 5, + KEY_UP: 6, + EVENT_BAG: 7, + MOUSE_DBLCLK: 8, + + // use java AWT key modifier masks + SHIFT_KEY: 64, + CTRL_KEY: 128, + META_KEY: 256, + ALT_KEY: 512, + SHIFT_LEFT: 1024, + CTRL_LEFT: 2048, + ALT_LEFT: 4096, + + // keycode + KEYCODE_SHIFT: 16, + KEYCODE_MULTIPLY: 106, + KEYCODE_ADD: 107, + KEYCODE_8: 56, + + CHARCODE_NUMPAD_MULTIPLY: 42, + CHARCODE_NUMPAD_ADD: 43, + + EVENT_QUEUE_MOUSE_EVENT: 1, + EVENT_QUEUE_KEYBOARD_EVENT: 2, + + STATUS_RECEIVING: 1, + STATUS_RECEIVED: 2, + STATUS_SENDING: 3, + STATUS_SENT: 4, + + setDirty: function(value) { + this.dirty = value; + }, + + isDirty: function() { + return this.dirty; + }, + + isImageLoaded: function() { + return this.imageLoaded; + }, + + refresh: function(imageUrl, tileMap, fullImage) { + var ajaxViewer = this; + var img = $(this.img); + this.fullImage = fullImage; + this.imgUrl=imageUrl; + + img.attr('src', imageUrl).load(function() { + ajaxViewer.imageLoaded = true; + }); + this.tileMap = tileMap; + }, + + resize: function(panelId, width, height, tileWidth, tileHeight) { + $(".canvas_tile", document.body).each(function() { + $(this).remove(); + }); + $("table", $("#" + panelId)).remove(); + + this.width = width; + this.height = height; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + this.panel = this.generateCanvas(panelId, width, height, tileWidth, tileHeight); + }, + + start: function() { + var ajaxViewer = this; + this.timer = setInterval(function() { ajaxViewer.heartbeat(); }, 50); + + $(document).bind("ajaxError", function(event, XMLHttpRequest, ajaxOptions, thrownError) { + ajaxViewer.onAjaxError(event, XMLHttpRequest, ajaxOptions, thrownError); + }); + + this.eventQueue = []; // reset event queue + this.sendingEventInProgress = false; + ajaxViewer.installMouseHook(); + ajaxViewer.installKeyboardHook(); + + + $(window).bind("resize", function() { + ajaxViewer.onWindowResize(); + }); + }, + + stop: function() { + clearInterval(this.timer); + this.deleteCanvas(); + + this.uninstallMouseHook(); + this.uninstallKeyboardHook(); + this.eventQueue = []; + this.sendingEventInProgress = false; + + $(document).unbind("ajaxError"); + $(window).unbind("resize"); + }, + + sendMouseEvent: function(event, x, y, whichButton, modifiers) { + this.eventQueue.push({ + type: this.EVENT_QUEUE_MOUSE_EVENT, + event: event, + x: x, + y: y, + code: whichButton, + modifiers: modifiers + }); + this.checkEventQueue(); + }, + + setupKeyCodeTranslationTable: function() { + this.keyCodeMap = {}; + for(var i = 'a'.charCodeAt(); i < 'z'.charCodeAt(); i++) + this.keyCodeMap[i] = { code: 65 + i - 'a'.charCodeAt(), shift: false }; + for(i = 'A'.charCodeAt(); i < 'Z'.charCodeAt(); i++) + this.keyCodeMap[i] = { code: 65 + i - 'A'.charCodeAt(), shift: true }; + for(i = '0'.charCodeAt(); i < '9'.charCodeAt(); i++) + this.keyCodeMap[i] = { code: 48 + i - '0'.charCodeAt(), shift: false }; + + this.keyCodeMap['`'.charCodeAt()] = { code : 192, shift : false }; + this.keyCodeMap['~'.charCodeAt()] = { code : 192, shift : true }; + + this.keyCodeMap[')'.charCodeAt()] = { code : 48, shift : true }; + this.keyCodeMap['!'.charCodeAt()] = { code : 49, shift : true }; + this.keyCodeMap['@'.charCodeAt()] = { code : 50, shift : true }; + this.keyCodeMap['#'.charCodeAt()] = { code : 51, shift : true }; + this.keyCodeMap['$'.charCodeAt()] = { code : 52, shift : true }; + this.keyCodeMap['%'.charCodeAt()] = { code : 53, shift : true }; + this.keyCodeMap['^'.charCodeAt()] = { code : 54, shift : true }; + this.keyCodeMap['&'.charCodeAt()] = { code : 55, shift : true }; + this.keyCodeMap['*'.charCodeAt()] = { code : 56, shift : true }; + this.keyCodeMap['('.charCodeAt()] = { code : 57, shift : true }; + + this.keyCodeMap['-'.charCodeAt()] = { code : 109, shift : false }; + this.keyCodeMap['_'.charCodeAt()] = { code : 109, shift : true }; + this.keyCodeMap['='.charCodeAt()] = { code : 107, shift : false }; + this.keyCodeMap['+'.charCodeAt()] = { code : 107, shift : true }; + + this.keyCodeMap['['.charCodeAt()] = { code : 219, shift : false }; + this.keyCodeMap['{'.charCodeAt()] = { code : 219, shift : true }; + this.keyCodeMap[']'.charCodeAt()] = { code : 221, shift : false }; + this.keyCodeMap['}'.charCodeAt()] = { code : 221, shift : true }; + this.keyCodeMap['\\'.charCodeAt()] = { code : 220, shift : false }; + this.keyCodeMap['|'.charCodeAt()] = { code : 220, shift : true }; + this.keyCodeMap[';'.charCodeAt()] = { code : 59, shift : false }; + this.keyCodeMap[':'.charCodeAt()] = { code : 59, shift : true }; + this.keyCodeMap['\''.charCodeAt()] = { code : 222 , shift : false }; + this.keyCodeMap['"'.charCodeAt()] = { code : 222, shift : true }; + this.keyCodeMap[','.charCodeAt()] = { code : 188 , shift : false }; + this.keyCodeMap['<'.charCodeAt()] = { code : 188, shift : true }; + this.keyCodeMap['.'.charCodeAt()] = { code : 190, shift : false }; + this.keyCodeMap['>'.charCodeAt()] = { code : 190, shift : true }; + this.keyCodeMap['/'.charCodeAt()] = { code : 191, shift : false }; + this.keyCodeMap['?'.charCodeAt()] = { code : 191, shift : true }; + }, + + // Firefox on Mac OS X does not generate key-code for following keys + translateZeroKeycode: function() { + var len = this.eventQueue.length; + if(len > 1 && this.eventQueue[len - 2].type == this.EVENT_QUEUE_KEYBOARD_EVENT && this.eventQueue[len - 2].code == 0) { + switch(this.eventQueue[len - 1].code) { + case 95 : // underscore _ + this.eventQueue[len - 2].code = 109; + break; + + case 58 : // colon : + this.eventQueue[len - 2].code = 59; + break; + + case 60 : // < + this.eventQueue[len - 2].code = 188; + break; + + case 62 : // > + this.eventQueue[len - 2].code = 190; + break; + + case 63 : // ? + this.eventQueue[len - 2].code = 191; + break; + + case 124 : // | + this.eventQueue[len - 2].code = 220; + break; + + case 126 : // ~ + this.eventQueue[len - 2].code = 192; + break; + + default : + g_logger.log(Logger.LEVEL_WARN, "Zero keycode detected for KEY-PRESS char code " + this.eventQueue[len - 1].code); + break; + } + } + }, + + // + // Firefox on Mac OS X does not send KEY-DOWN for repeated KEY-PRESS event + // IE on windows, when typing is fast, it will omit issuing KEY-DOWN event + // + translateImcompletedKeypress : function() { + var len = this.eventQueue.length; + if(len == 1 || !(this.eventQueue[len - 2].type == this.EVENT_QUEUE_KEYBOARD_EVENT && this.eventQueue[len - 2].event == this.KEY_DOWN)) { + var nSplicePos = Math.max(0, len - 2); + var keyPressEvent = this.eventQueue[len - 1]; + if(!!this.keyCodeMap[keyPressEvent.code]) { + if(this.keyCodeMap[keyPressEvent.code].shift) { + this.eventQueue.splice(nSplicePos, 0, { + type: this.EVENT_QUEUE_KEYBOARD_EVENT, + event: this.KEY_DOWN, + code: this.keyCodeMap[keyPressEvent.code].code, + modifiers: this.SHIFT_KEY + }); + } else { + this.eventQueue.splice(nSplicePos, 0, { + type: this.EVENT_QUEUE_KEYBOARD_EVENT, + event: this.KEY_DOWN, + code: this.keyCodeMap[keyPressEvent.code].code, + modifiers: 0 + }); + } + } else { + g_logger.log(Logger.LEVEL_WARN, "Keycode mapping is not defined to translate KEY-PRESS event for char code : " + keyPressEvent.code); + this.eventQueue.splice(nSplicePos, 0, { + type: this.EVENT_QUEUE_KEYBOARD_EVENT, + event: this.KEY_DOWN, + code: keyPressEvent.code, + modifiers: keyPressEvent.modifiers + }); + } + } + }, + + sendKeyboardEvent: function(event, code, modifiers) { + // back door to open logger window - CTRL-ATL-SHIFT+SPACE + if(code == 32 && + (modifiers & this.SHIFT_KEY | this.CTRL_KEY | this.ALT_KEY) == (this.SHIFT_KEY | this.CTRL_KEY | this.ALT_KEY)) { + g_logger.enable(true); + g_logger.open(); + } + + var len; + g_logger.log(Logger.LEVEL_INFO, "Keyboard event: " + event + ", code: " + code + ", modifiers: " + modifiers + ', char: ' + String.fromCharCode(code)); + this.eventQueue.push({ + type: this.EVENT_QUEUE_KEYBOARD_EVENT, + event: event, + code: code, + modifiers: modifiers + }); + + if(event == this.KEY_PRESS) { + this.translateZeroKeycode(); + this.translateImcompletedKeypress(); + } + + if(this.rawKeyboard) { + if(event == this.KEY_PRESS) { + // special handling for key * in numeric pad area + if(code == this.CHARCODE_NUMPAD_MULTIPLY) { + len = this.eventQueue.length; + if(len >= 2) { + var origKeyDown = this.eventQueue[len - 2]; + if(origKeyDown.type == this.EVENT_QUEUE_KEYBOARD_EVENT && + origKeyDown.code == this.KEYCODE_MULTIPLY) { + + this.eventQueue.splice(len - 2, 2, { + type: this.EVENT_QUEUE_KEYBOARD_EVENT, + event: this.KEY_DOWN, + code: this.KEYCODE_SHIFT, + modifiers: 0 + }, + { + type: this.EVENT_QUEUE_KEYBOARD_EVENT, + event: this.KEY_DOWN, + code: this.KEYCODE_8, + modifiers: this.SHIFT_KEY + }, + { + type: this.EVENT_QUEUE_KEYBOARD_EVENT, + event: this.KEY_UP, + code: this.KEYCODE_8, + modifiers: this.SHIFT_KEY + }, + { + type: this.EVENT_QUEUE_KEYBOARD_EVENT, + event: this.KEY_UP, + code: this.KEYCODE_SHIFT, + modifiers: 0 + } + ); + } + } + return; + } + + // special handling for key + in numeric pad area + if(code == this.CHARCODE_NUMPAD_ADD) { + len = this.eventQueue.length; + if(len >= 2) { + var origKeyDown = this.eventQueue[len - 2]; + if(origKeyDown.type == this.EVENT_QUEUE_KEYBOARD_EVENT && + origKeyDown.code == this.KEYCODE_ADD) { + + g_logger.log(Logger.LEVEL_INFO, "Detected + on numeric pad area, fake it"); + this.eventQueue[len - 2].modifiers = this.SHIFT_KEY; + this.eventQueue[len - 1].modifiers = this.SHIFT_KEY; + this.eventQueue.splice(len - 2, 0, { + type: this.EVENT_QUEUE_KEYBOARD_EVENT, + event: this.KEY_DOWN, + code: this.KEYCODE_SHIFT, + modifiers: this.SHIFT_KEY + }); + this.eventQueue.push({ + type: this.EVENT_QUEUE_KEYBOARD_EVENT, + event: this.KEY_UP, + code: this.KEYCODE_SHIFT, + modifiers: this.SHIFT_KEY + }); + } + } + } + } + + if(event != this.KEY_DOWN) + this.checkEventQueue(); + } else { + this.checkEventQueue(); + } + }, + + aggregateEvents: function() { + var ajaxViewer = this; + var aggratedQueue = []; + + var aggregating = false; + var mouseX; + var mouseY; + $.each(ajaxViewer.eventQueue, function(index, item) { + if(item.type != ajaxViewer.EVENT_QUEUE_MOUSE_EVENT) { + aggratedQueue.push(item); + } else { + if(!aggregating) { + if(item.event == ajaxViewer.MOUSE_MOVE) { + aggregating = true; + mouseX = item.x; + mouseY = item.y; + } else { + aggratedQueue.push(item); + } + } else { + if(item.event == ajaxViewer.MOUSE_MOVE) { + // continue to aggregate mouse move event + mouseX = item.x; + mouseY = item.y; + } else { + aggratedQueue.push({ + type: ajaxViewer.EVENT_QUEUE_MOUSE_EVENT, + event: ajaxViewer.MOUSE_MOVE, + x: mouseX, + y: mouseY, + code: 0, + modifiers: 0 + }); + aggregating = false; + + aggratedQueue.push(item); + } + } + } + }); + + if(aggregating) { + aggratedQueue.push({ + type: ajaxViewer.EVENT_QUEUE_MOUSE_EVENT, + event: ajaxViewer.MOUSE_MOVE, + x: mouseX, + y: mouseY, + code: 0, + modifiers: 0 + }); + } + + this.eventQueue = aggratedQueue; + }, + + checkEventQueue: function() { + var ajaxViewer = this; + + if(!this.sendingEventInProgress && this.eventQueue.length > 0) { + var sb = new StringBuilder(); + sb.append(""+this.eventQueue.length).append("|"); + $.each(this.eventQueue, function() { + var item = this; + if(item.type == ajaxViewer.EVENT_QUEUE_MOUSE_EVENT) { + sb.append(""+item.type).append("|"); + sb.append(""+item.event).append("|"); + sb.append(""+item.x).append("|"); + sb.append(""+item.y).append("|"); + sb.append(""+item.code).append("|"); + sb.append(""+item.modifiers).append("|"); + } else { + sb.append(""+item.type).append("|"); + sb.append(""+item.event).append("|"); + sb.append(""+item.code).append("|"); + sb.append(""+item.modifiers).append("|"); + } + }); + this.eventQueue.length = 0; + + var url = ajaxViewer.updateUrl + "&event=" + ajaxViewer.EVENT_BAG; + + g_logger.log(Logger.LEVEL_TRACE, "Posting client event " + sb.toString() + "..."); + + ajaxViewer.sendingEventInProgress = true; + window.onStatusNotify(ajaxViewer.STATUS_SENDING); + $.post(url, {data: sb.toString()}, function(data, textStatus) { + g_logger.log(Logger.LEVEL_TRACE, "Client event " + sb.toString() + " is posted"); + + ajaxViewer.sendingEventInProgress = false; + window.onStatusNotify(ajaxViewer.STATUS_SENT); + + ajaxViewer.checkUpdate(); + }, 'html'); + } + }, + + onAjaxError: function(event, XMLHttpRequest, ajaxOptions, thrownError) { + if(window.onClientError != undefined && jQuery.isFunction(window.onClientError)) { + window.onClientError(); + } + }, + + onWindowResize: function() { + var offset = this.panel.offset(); + + var row = $('tr:first', this.panel); + var cell = $('td:first', row); + var tile = this.getTile(cell, 'tile'); + + var tileOffset = tile.offset(); + var deltaX = offset.left - tileOffset.left; + var deltaY = offset.top - tileOffset.top; + + if(deltaX != 0 || deltaY != 0) { + $(".canvas_tile").each(function() { + var offsetFrom = $(this).offset(); + $(this).css('left', offsetFrom.left + deltaX).css('top', offsetFrom.top + deltaY); + }); + } + }, + + deleteCanvas: function() { + $('.canvas_tile', $(document.body)).each(function() { + $(this).remove(); + }); + }, + + generateCanvas: function(wrapperDivId, width, height, tileWidth, tileHeight) { + var canvasParent = $('#' + wrapperDivId); + canvasParent.width(width); + canvasParent.height(height); + + if(window.onCanvasSizeChange != undefined && jQuery.isFunction(window.onCanvasSizeChange)) + window.onCanvasSizeChange(width, height); + + var tableDef = '\r\n'; + var i = 0; + var j = 0; + for(i = 0; i < Math.ceil((height + tileHeight - 1) / tileHeight); i++) { + var rowHeight = Math.min(height - i*tileHeight, tileHeight); + tableDef += '\r\n'; + + for(j = 0; j < Math.ceil((width + tileWidth - 1) / tileWidth); j++) { + var colWidth = Math.min(width - j*tileWidth, tileWidth); + tableDef += '\r\n'; + } + tableDef += '\r\n'; + } + tableDef += '
\r\n'; + + return $(tableDef).appendTo(canvasParent); + }, + + getTile: function(cell, name) { + var clonedDiv = cell.data(name); + if(!clonedDiv) { + var offset = cell.offset(); + var divDef = "
"; + + clonedDiv = $(divDef).appendTo($(document.body)); + cell.data(name, clonedDiv); + } + + return clonedDiv; + }, + + initCell: function(cell) { + if(!cell.data("init")) { + cell.data("init", true); + + cell.data("current", 0); + this.getTile(cell, "tile2"); + this.getTile(cell, "tile"); + } + }, + + displayCell: function(cell, bg) { + var div; + var divPrev; + if(!cell.data("current")) { + cell.data("current", 1); + + divPrev = this.getTile(cell, "tile"); + div = this.getTile(cell, "tile2"); + } else { + cell.data("current", 0); + divPrev = this.getTile(cell, "tile2"); + div = this.getTile(cell, "tile"); + } + + div.css("z-index", parseInt(divPrev.css("z-index")) + 1); + div.css("background", bg); + }, + + updateTile: function() { + if(this.dirty) { + var ajaxViewer = this; + var tileWidth = this.tileWidth; + var tileHeight = this.tileHeight; + var imgUrl = this.imgUrl; + var panel = this.panel; + + if(this.fullImage) { + $.each(this.tileMap, function() { + var i = $(this)[0]; + var j = $(this)[1]; + var row = $("TR:eq("+i+")", panel); + var cell = $("TD:eq("+j+")", row); + var attr = "url(" + imgUrl + ") -"+j*tileWidth+"px -"+i*tileHeight + "px"; + + ajaxViewer.initCell(cell); + ajaxViewer.displayCell(cell, attr); + }); + } else { + $.each(this.tileMap, function(index) { + var i = $(this)[0]; + var j = $(this)[1]; + var offset = index*tileWidth; + var attr = "url(" + imgUrl + ") no-repeat -"+offset+"px 0px"; + var row = $("TR:eq("+i+")", panel); + var cell = $("TD:eq("+j+")", row); + + ajaxViewer.initCell(cell); + ajaxViewer.displayCell(cell, attr); + }); + } + + this.dirty = false; + } + }, + + heartbeat: function() { + this.checkEventQueue(); + this.checkUpdate(); + }, + + checkUpdate: function() { + if(!this.isDirty()) + return; + + if(this.isImageLoaded()) { + this.updateTile(); + var url = this.updateUrl; + var ajaxViewer = this; + + window.onStatusNotify(ajaxViewer.STATUS_RECEIVING); + $.getScript(url, function(data, textStatus) { + if(/^/.test(data)) { + ajaxViewer.stop(); + $(document.body).html(data); + } else { + eval(data); + ajaxViewer.setDirty(true); + window.onStatusNotify(ajaxViewer.STATUS_RECEIVED); + + ajaxViewer.checkUpdate(); + } + }); + } + }, + + ptInPanel: function(pageX, pageY) { + var mainPanel = this.panel; + + var offset = mainPanel.offset(); + var x = pageX - offset.left; + var y = pageY - offset.top; + + if(x < 0 || y < 0 || x > mainPanel.width() - 1 || y > mainPanel.height() - 1) + return false; + return true; + }, + + pageToPanel: function(pageX, pageY) { + var mainPanel = this.panel; + + var offset = mainPanel.offset(); + var x = pageX - offset.left; + var y = pageY - offset.top; + + if(x < 0) + x = 0; + if(x > mainPanel.width() - 1) + x = mainPanel.width() - 1; + + if(y < 0) + y = 0; + if(y > mainPanel.height() - 1) + y = mainPanel.height() - 1; + + return { x: Math.ceil(x), y: Math.ceil(y) }; + }, + + installMouseHook: function() { + var ajaxViewer = this; + var target = $(document); + + target.mousemove(function(e) { + if(!ajaxViewer.ptInPanel(e.pageX, e.pageY)) + return true; + + var pt = ajaxViewer.pageToPanel(e.pageX, e.pageY); + ajaxViewer.onMouseMove(pt.x, pt.y); + + e.stopPropagation(); + return false; + }); + + target.mousedown(function(e) { + ajaxViewer.panel.parent().focus(); + + if(!ajaxViewer.ptInPanel(e.pageX, e.pageY)) + return true; + + var modifiers = ajaxViewer.getKeyModifiers(e); + var whichButton = e.button; + + var pt = ajaxViewer.pageToPanel(e.pageX, e.pageY); + ajaxViewer.onMouseDown(pt.x, pt.y, whichButton, modifiers); + + e.stopPropagation(); + return false; + }); + + target.mouseup(function(e) { + if(!ajaxViewer.ptInPanel(e.pageX, e.pageY)) + return true; + + var modifiers = ajaxViewer.getKeyModifiers(e); + var whichButton = e.button; + + var pt = ajaxViewer.pageToPanel(e.pageX, e.pageY); + ajaxViewer.onMouseUp(pt.x, pt.y, whichButton, modifiers); + e.stopPropagation(); + return false; + }); + + // disable browser right-click context menu + target.bind("contextmenu", function() { return false; }); + }, + + uninstallMouseHook : function() { + var target = $(document); + target.unbind("mousemove"); + target.unbind("mousedown"); + target.unbind("mouseup"); + target.unbind("contextmenu"); + }, + + requiresDefaultKeyProcess : function(e) { + switch(e.which) { + case 8 : // backspace + case 9 : // TAB + case 19 : // PAUSE/BREAK + case 20 : // CAPSLOCK + case 27 : // ESCAPE + case 16 : // SHIFT key + case 17 : // CTRL key + case 18 : // ALT key + case 33 : // PGUP + case 34 : // PGDN + case 35 : // END + case 36 : // HOME + case 37 : // LEFT + case 38 : // UP + case 39 : // RIGHT + case 40 : // DOWN + return false; + } + + if(this.getKeyModifiers(e) == this.SHIFT_KEY) + return true; + + if(this.getKeyModifiers(e) != 0) + return false; + + return true; + }, + + installKeyboardHook: function() { + var ajaxViewer = this; + var target = $(document); + + target.keypress(function(e) { + ajaxViewer.onKeyPress(e.which, ajaxViewer.getKeyModifiers(e)); + + e.stopPropagation(); + if(ajaxViewer.requiresDefaultKeyProcess(e)) + return true; + + e.preventDefault(); + return false; + }); + + target.keydown(function(e) { + ajaxViewer.onKeyDown(e.which, ajaxViewer.getKeyModifiers(e)); + + e.stopPropagation(); + if(ajaxViewer.requiresDefaultKeyProcess(e)) + return true; + + e.preventDefault(); + return false; + }); + + target.keyup(function(e) { + ajaxViewer.onKeyUp(e.which, ajaxViewer.getKeyModifiers(e)); + + e.stopPropagation(); + if(ajaxViewer.requiresDefaultKeyProcess(e)) + return true; + + e.preventDefault(); + return false; + }); + }, + + uninstallKeyboardHook : function() { + var target = $(document); + target.unbind("keypress"); + target.unbind("keydown"); + target.unbind("keyup"); + }, + + onMouseMove: function(x, y) { + this.sendMouseEvent(this.MOUSE_MOVE, x, y, 0, 0); + }, + + onMouseDown: function(x, y, whichButton, modifiers) { + this.sendMouseEvent(this.MOUSE_DOWN, x, y, whichButton, modifiers); + }, + + onMouseUp: function(x, y, whichButton, modifiers) { + this.sendMouseEvent(this.MOUSE_UP, x, y, whichButton, modifiers); + + var curTick = new Date().getTime(); + if(this.lastClickEvent.time && (curTick - this.lastClickEvent.time < 300)) { + this.onMouseDblClick(this.lastClickEvent.x, this.lastClickEvent.y, + this.lastClickEvent.button, this.lastClickEvent.modifiers); + } + + this.lastClickEvent.x = x; + this.lastClickEvent.y = y; + this.lastClickEvent.button = whichButton; + this.lastClickEvent.modifiers = modifiers; + this.lastClickEvent.time = curTick; + }, + + onMouseDblClick: function(x, y, whichButton, modifiers) { + this.sendMouseEvent(this.MOUSE_DBLCLK, x, y, whichButton, modifiers); + }, + + onKeyPress: function(code, modifiers) { + this.sendKeyboardEvent(this.KEY_PRESS, code, modifiers); + }, + + onKeyDown: function(code, modifiers) { + this.sendKeyboardEvent(this.KEY_DOWN, code, modifiers); + }, + + onKeyUp: function(code, modifiers) { + this.sendKeyboardEvent(this.KEY_UP, code, modifiers); + }, + + getKeyModifiers: function(e) { + var modifiers = 0; + if(e.altKey) + modifiers |= this.ALT_KEY; + + if(e.altLeft) + modifiers |= this.ALT_LEFT; + + if(e.ctrlKey) + modifiers |= this.CTRL_KEY; + + if(e.ctrlLeft) + modifiers |= this.CTRL_LEFT; + + if(e.shiftKey) + modifiers |= this.SHIFT_KEY; + + if(e.shiftLeft) + modifiers |= this.SHIFT_LEFT; + + if(e.metaKey) + modifiers |= this.META_KEY; + + return modifiers; + } +}; diff --git a/console-proxy/js/cloud.logger.js b/console-proxy/js/cloud.logger.js new file mode 100644 index 00000000000..895e421ce91 --- /dev/null +++ b/console-proxy/js/cloud.logger.js @@ -0,0 +1,288 @@ + /** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// Version: 1.9.1.152 + +// +// Javascript logger utility +// Author +// Kelven Yang +// 2/25/2010 +// + +function Logger() { + this.bDockEnabled = true; + + this.logWin = null; + this.logger = null; + this.header = null; + + this.bEnabled = true; + this.level = 0; + + this.bMoving = false; + this.offsetStart = {left: 0, top: 0}; + this.ptStart = {x: 0, y: 0}; +} + +Logger.DEFAULT_WIN_HEIGHT = 500; +Logger.LEVEL_TRACE = 0; +Logger.LEVEL_DEBUG = 1; +Logger.LEVEL_INFO = 2; +Logger.LEVEL_WARN = 3; +Logger.LEVEL_ERROR = 4; +Logger.LEVEL_FATAL = 5; +Logger.LEVEL_SYS = 100; + +Logger.prototype = { + + open: function() { + var logger = this; + var logWinMarkup = [ + '
', + '
', + '
', + '', + '', + '', + '
', + '', + '
', + '
', + '
', + '', + '', + '
', + '
', + '
', + '
' + ].join(''); + + this.logWin = $(logWinMarkup).appendTo(document.body); + this.header = $('.logwin_title:first', this.logWin); + this.logger = $('.logwin_content:first', this.logWin); + + $(".logwin_title", this.logWin).mousedown(function(e) { + if($(e.target).attr('cmd')) + return true; + + if(!logger.bMoving) { + logger.bMoving = true; + logger.offsetStart = logger.logWin.offset(); + logger.ptStart = {x: e.pageX, y: e.pageY}; + + $(document).bind("mousemove", function(e) { + if(logger.bMoving) { + logger.enableDocking(false); + + var logWinNewLeft = logger.offsetStart.left + e.pageX - logger.ptStart.x; + var logWinNewTop = logger.offsetStart.top + e.pageY - logger.ptStart.y; + + logger.logWin.css("left", logWinNewLeft + "px").css("top", logWinNewTop + "px"); + } + return false; + }); + + $(document).bind("mouseup", function(e) { + if(logger.bMoving) { + logger.bMoving = false; + $(document).unbind("mousemove", arguments.callee.name); + $(document).unbind("mouseup", arguments.callee.name); + + return false; + } + return true; + }); + } + + // prevent default handling + return false; + }).dblclick(function(e) { + logger.expand(!logger.isExpanded()); + }); + + this.logWin.click(function(e) { + if($(e.target).attr('cmd')) { + switch($(e.target).attr('cmd')) { + case '1' : + logger.enable(true); + break; + + case '2' : + logger.enable(false); + break; + + case '3' : + logger.clear(); + break; + + case '4' : + logger.enableDocking(true); + logger.dockIn(); + break; + + case '5' : + logger.expand(!logger.isExpanded()); + break; + + default : + break; + } + } + }); + + $("#template_type", this.logWin).change(function(e) { + logger.setLevel(parseInt($(this).val())); + }); + + this.logWin.css("left", (($(document.body).width() - this.logWin.width()) / 2) + "px"); + this.dockIn(); + + this.log(Logger.LEVEL_SYS, "Logger started"); + }, + + dockIn: function() { + var logger = this; + var offset = this.logWin.offset(); + var bottom = offset.top + this.logWin.height(); + var delta = bottom - 2; + + this.logWin.animate({top: (offset.top - delta) + "px"}, 200, + function() { + logger.logWin.unbind("mouseleave"); + logger.logWin.bind("mouseenter", function(e) { + if(logger.bDockEnabled) + logger.dockOut(); + }); + } + ); + }, + + dockOut: function() { + var logger = this; + this.logWin.animate({top: "0px"}, 200, + function() { + logger.logWin.unbind("mouseenter"); + logger.logWin.bind("mouseleave", function(e) { + if(logger.bDockEnabled) { + var xPosInLogWin = e.pageX - logger.logWin.offset().left; + var yPosInLogWin = e.pageY - logger.logWin.offset().top; + + if(xPosInLogWin < 0 || yPosInLogWin < 0 || + xPosInLogWin > logger.logWin.width() || yPosInLogWin > logger.logWin.height()) { + logger.dockIn(); + } + } + }); + } + ); + }, + + enableDocking: function(bEnable) { + this.bDockEnabled = bEnable; + }, + + log: function(level, message) { + // Note : LEVEL_SYS message will always be logged + if(this.logger && (level == Logger.LEVEL_SYS || this.bEnabled && level >= this.level)) { + var curTime = new Date(); + var curTimeString = [ + '', curTime.getMonth(), + '/', curTime.getDate(), + '/', curTime.getYear(), + ' ', + curTime.getHours(), + ':', curTime.getMinutes(), + ":", curTime.getSeconds(), + ".", curTime.getMilliseconds()].join(''); + + this.logger.append(this.getLevelDisplayString(level) + " - " + curTimeString + " - " + message + '
'); + } + }, + + clear: function() { + if(this.logger) { + this.logger.empty(); + this.log(Logger.LEVEL_SYS, "Logger is cleared"); + } + }, + + setLevel: function(level) { + this.level = level; + + this.log(Logger.LEVEL_SYS, "Set logger trace level to " + this.getLevelDisplayString(level)); + }, + + enable: function(bEnabled) { + this.bEnabled = bEnabled; + + if(bEnabled) + this.log(Logger.LEVEL_SYS, "Logger is enabled"); + else + this.log(Logger.LEVEL_SYS, "Logger is disabled"); + }, + + expand: function(bExpand) { + if(bExpand) { + this.logWin.height(Logger.DEFAULT_WIN_HEIGHT); + this.logger.height(Logger.DEFAULT_WIN_HEIGHT - this.header.height()); + } else { + this.logWin.height(this.header.height()); + this.logger.height(0); + } + }, + + isExpanded: function() { + return this.logWin.height() > this.header.height(); + }, + + getLevelDisplayString: function(level) { + switch(level) { + case Logger.LEVEL_TRACE : + return "TRACE"; + + case Logger.LEVEL_DEBUG : + return "DEBUG"; + + case Logger.LEVEL_INFO : + return "INFO"; + + case Logger.LEVEL_WARN : + return "WARN"; + + case Logger.LEVEL_ERROR : + return "ERROR"; + + case Logger.LEVEL_FATAL : + return "FATAL"; + + case Logger.LEVEL_SYS : + return "SYSINFO"; + } + + return "LEVEL " + level; + } +}; + diff --git a/console-proxy/js/handler.js b/console-proxy/js/handler.js new file mode 100644 index 00000000000..3043e024b06 --- /dev/null +++ b/console-proxy/js/handler.js @@ -0,0 +1,71 @@ + /** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// +// Callback handlers for AJAX viewer +// Author +// Kelven Yang +// 11/18/2009 +// +function onKickoff() { + ajaxViewer.stop(); + $('#toolbar').remove(); + $('#main_panel').html('

This session is terminated because a session for the same VM has been created elsewhere.

'); +} + +function onDisconnect() { + ajaxViewer.stop(); + $('#toolbar').remove(); + $('#main_panel').html('

This session is terminated as the machine you are accessing has terminated the connection.

'); +} + +function onClientError() { + ajaxViewer.stop(); + $('#toolbar').remove(); + $('#main_panel').html('

Client communication error, please retry later.

'); +} + +function onCanvasSizeChange(width, height) { + $('#toolbar').width(width); +} + +function onStatusNotify(status) { + if(status == ajaxViewer.STATUS_SENDING || status == ajaxViewer.STATUS_RECEIVING) + $('#light').removeClass('dark').addClass('bright'); + else + $('#light').removeClass('bright').addClass('dark'); +} + +function sendCtrlAltDel() { + ajaxViewer.sendKeyboardEvent(ajaxViewer.KEY_DOWN, 45, ajaxViewer.CTRL_KEY | ajaxViewer.ALT_KEY); + ajaxViewer.sendKeyboardEvent(ajaxViewer.KEY_UP, 45, ajaxViewer.CTRL_KEY | ajaxViewer.ALT_KEY); +} + +function sendCtrlEsc() { + ajaxViewer.sendKeyboardEvent(ajaxViewer.KEY_DOWN, 17, 0); + ajaxViewer.sendKeyboardEvent(ajaxViewer.KEY_DOWN, 27, ajaxViewer.CTRL_KEY); + ajaxViewer.sendKeyboardEvent(ajaxViewer.KEY_UP, 27, ajaxViewer.CTRL_KEY); + ajaxViewer.sendKeyboardEvent(ajaxViewer.KEY_UP, 17, 0); +} + +function sendAltTab() { + ajaxViewer.sendKeyboardEvent(ajaxViewer.KEY_DOWN, 18, 0); + ajaxViewer.sendKeyboardEvent(ajaxViewer.KEY_DOWN, 9, ajaxViewer.ALT_KEY); + ajaxViewer.sendKeyboardEvent(ajaxViewer.KEY_UP, 9, ajaxViewer.ALT_KEY); + ajaxViewer.sendKeyboardEvent(ajaxViewer.KEY_UP, 18, 0); +} diff --git a/console-proxy/js/jquery.js b/console-proxy/js/jquery.js new file mode 100644 index 00000000000..b1ae21d8b23 --- /dev/null +++ b/console-proxy/js/jquery.js @@ -0,0 +1,19 @@ +/* + * jQuery JavaScript Library v1.3.2 + * http://jquery.com/ + * + * Copyright (c) 2009 John Resig + * Dual licensed under the MIT and GPL licenses. + * http://docs.jquery.com/License + * + * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) + * Revision: 6246 + */ +(function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.isArray(E)?E:o.makeArray(E))},selector:"",jquery:"1.3.2",size:function(){return this.length},get:function(E){return E===g?Array.prototype.slice.call(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,sort:[].sort,splice:[].splice,find:function(E){if(this.length===1){var F=this.pushStack([],"find",E);F.length=0;o.find(E,this[0],F);return F}else{return this.pushStack(o.unique(o.map(this,function(G){return o.find(E,G)})),"find",E)}},clone:function(G){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.outerHTML;if(!I){var J=this.ownerDocument.createElement("div");J.appendChild(this.cloneNode(true));I=J.innerHTML}return o.clean([I.replace(/ jQuery\d+="(?:\d+|null)"/g,"").replace(/^\s*/,"")])[0]}else{return this.cloneNode(true)}});if(G===true){var H=this.find("*").andSelf(),F=0;E.find("*").andSelf().each(function(){if(this.nodeName!==H[F].nodeName){return}var I=o.data(H[F],"events");for(var K in I){for(var J in I[K]){o.event.add(this,K,I[K][J],I[K][J].data)}}F++})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var G=o.expr.match.POS.test(E)?o(E):null,F=0;return this.map(function(){var H=this;while(H&&H.ownerDocument){if(G?G.index(H)>-1:o(H).is(E)){o.data(H,"closest",F);return H}H=H.parentNode;F++}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g,""):null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=o.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild;if(H){for(var G=0,E=this.length;G1||G>0?I.cloneNode(true):I)}}if(F){o.each(F,z)}}return this;function K(N,O){return M&&o.nodeName(N,"table")&&o.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(H,F,J,E){if(F=="width"||F=="height"){var L,G={position:"absolute",visibility:"hidden",display:"block"},K=F=="width"?["Left","Right"]:["Top","Bottom"];function I(){L=F=="width"?H.offsetWidth:H.offsetHeight;if(E==="border"){return}o.each(K,function(){if(!E){L-=parseFloat(o.curCSS(H,"padding"+this,true))||0}if(E==="margin"){L+=parseFloat(o.curCSS(H,"margin"+this,true))||0}else{L-=parseFloat(o.curCSS(H,"border"+this+"Width",true))||0}})}if(H.offsetWidth!==0){I()}else{o.swap(H,G,I)}return Math.max(0,Math.round(L))}return o.curCSS(H,F,J)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,S){if(typeof S==="number"){S+=""}if(!S){return}if(typeof S==="string"){S=S.replace(/(<(\w+)[^>]*?)\/>/g,function(U,V,T){return T.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?U:V+">"});var O=S.replace(/^\s+/,"").substring(0,10).toLowerCase();var Q=!O.indexOf("",""]||!O.indexOf("",""]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
"]||!O.indexOf("",""]||(!O.indexOf("",""]||!O.indexOf("",""]||!o.support.htmlSerialize&&[1,"div
","
"]||[0,"",""];L.innerHTML=Q[1]+S+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var R=/"&&!R?L.childNodes:[];for(var M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(S)){L.insertBefore(K.createTextNode(S.match(/^\s*/)[0]),L.firstChild)}S=o.makeArray(L.childNodes)}if(S.nodeType){G.push(S)}else{G=o.merge(G,S)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E0?this.clone(true):this).get();o.fn[F].apply(o(L[K]),I);J=J.concat(I)}return this.pushStack(J,E,G)}});o.each({removeAttr:function(E){o.attr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(this).children().remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}}); +/* + * Sizzle CSS Selector Engine - v0.9.3 + * Copyright 2009, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){var R=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,L=0,H=Object.prototype.toString;var F=function(Y,U,ab,ac){ab=ab||[];U=U||document;if(U.nodeType!==1&&U.nodeType!==9){return[]}if(!Y||typeof Y!=="string"){return ab}var Z=[],W,af,ai,T,ad,V,X=true;R.lastIndex=0;while((W=R.exec(Y))!==null){Z.push(W[1]);if(W[2]){V=RegExp.rightContext;break}}if(Z.length>1&&M.exec(Y)){if(Z.length===2&&I.relative[Z[0]]){af=J(Z[0]+Z[1],U)}else{af=I.relative[Z[0]]?[U]:F(Z.shift(),U);while(Z.length){Y=Z.shift();if(I.relative[Y]){Y+=Z.shift()}af=J(Y,af)}}}else{var ae=ac?{expr:Z.pop(),set:E(ac)}:F.find(Z.pop(),Z.length===1&&U.parentNode?U.parentNode:U,Q(U));af=F.filter(ae.expr,ae.set);if(Z.length>0){ai=E(af)}else{X=false}while(Z.length){var ah=Z.pop(),ag=ah;if(!I.relative[ah]){ah=""}else{ag=Z.pop()}if(ag==null){ag=U}I.relative[ah](ai,ag,Q(U))}}if(!ai){ai=af}if(!ai){throw"Syntax error, unrecognized expression: "+(ah||Y)}if(H.call(ai)==="[object Array]"){if(!X){ab.push.apply(ab,ai)}else{if(U.nodeType===1){for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&(ai[aa]===true||ai[aa].nodeType===1&&K(U,ai[aa]))){ab.push(af[aa])}}}else{for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&ai[aa].nodeType===1){ab.push(af[aa])}}}}}else{E(ai,ab)}if(V){F(V,U,ab,ac);if(G){hasDuplicate=false;ab.sort(G);if(hasDuplicate){for(var aa=1;aa":function(Z,U,aa){var X=typeof U==="string";if(X&&!/\W/.test(U)){U=aa?U:U.toUpperCase();for(var V=0,T=Z.length;V=0)){if(!V){T.push(Y)}}else{if(V){U[X]=false}}}}return false},ID:function(T){return T[1].replace(/\\/g,"")},TAG:function(U,T){for(var V=0;T[V]===false;V++){}return T[V]&&Q(T[V])?U[1]:U[1].toUpperCase()},CHILD:function(T){if(T[1]=="nth"){var U=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(T[2]=="even"&&"2n"||T[2]=="odd"&&"2n+1"||!/\D/.test(T[2])&&"0n+"+T[2]||T[2]);T[2]=(U[1]+(U[2]||1))-0;T[3]=U[3]-0}T[0]=L++;return T},ATTR:function(X,U,V,T,Y,Z){var W=X[1].replace(/\\/g,"");if(!Z&&I.attrMap[W]){X[1]=I.attrMap[W]}if(X[2]==="~="){X[4]=" "+X[4]+" "}return X},PSEUDO:function(X,U,V,T,Y){if(X[1]==="not"){if(X[3].match(R).length>1||/^\w/.test(X[3])){X[3]=F(X[3],null,null,U)}else{var W=F.filter(X[3],U,V,true^Y);if(!V){T.push.apply(T,W)}return false}}else{if(I.match.POS.test(X[0])||I.match.CHILD.test(X[0])){return true}}return X},POS:function(T){T.unshift(true);return T}},filters:{enabled:function(T){return T.disabled===false&&T.type!=="hidden"},disabled:function(T){return T.disabled===true},checked:function(T){return T.checked===true},selected:function(T){T.parentNode.selectedIndex;return T.selected===true},parent:function(T){return !!T.firstChild},empty:function(T){return !T.firstChild},has:function(V,U,T){return !!F(T[3],V).length},header:function(T){return/h\d/i.test(T.nodeName)},text:function(T){return"text"===T.type},radio:function(T){return"radio"===T.type},checkbox:function(T){return"checkbox"===T.type},file:function(T){return"file"===T.type},password:function(T){return"password"===T.type},submit:function(T){return"submit"===T.type},image:function(T){return"image"===T.type},reset:function(T){return"reset"===T.type},button:function(T){return"button"===T.type||T.nodeName.toUpperCase()==="BUTTON"},input:function(T){return/input|select|textarea|button/i.test(T.nodeName)}},setFilters:{first:function(U,T){return T===0},last:function(V,U,T,W){return U===W.length-1},even:function(U,T){return T%2===0},odd:function(U,T){return T%2===1},lt:function(V,U,T){return UT[3]-0},nth:function(V,U,T){return T[3]-0==U},eq:function(V,U,T){return T[3]-0==U}},filter:{PSEUDO:function(Z,V,W,aa){var U=V[1],X=I.filters[U];if(X){return X(Z,W,V,aa)}else{if(U==="contains"){return(Z.textContent||Z.innerText||"").indexOf(V[3])>=0}else{if(U==="not"){var Y=V[3];for(var W=0,T=Y.length;W=0)}}},ID:function(U,T){return U.nodeType===1&&U.getAttribute("id")===T},TAG:function(U,T){return(T==="*"&&U.nodeType===1)||U.nodeName===T},CLASS:function(U,T){return(" "+(U.className||U.getAttribute("class"))+" ").indexOf(T)>-1},ATTR:function(Y,W){var V=W[1],T=I.attrHandle[V]?I.attrHandle[V](Y):Y[V]!=null?Y[V]:Y.getAttribute(V),Z=T+"",X=W[2],U=W[4];return T==null?X==="!=":X==="="?Z===U:X==="*="?Z.indexOf(U)>=0:X==="~="?(" "+Z+" ").indexOf(U)>=0:!U?Z&&T!==false:X==="!="?Z!=U:X==="^="?Z.indexOf(U)===0:X==="$="?Z.substr(Z.length-U.length)===U:X==="|="?Z===U||Z.substr(0,U.length+1)===U+"-":false},POS:function(X,U,V,Y){var T=U[2],W=I.setFilters[T];if(W){return W(X,V,U,Y)}}}};var M=I.match.POS;for(var O in I.match){I.match[O]=RegExp(I.match[O].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(U,T){U=Array.prototype.slice.call(U);if(T){T.push.apply(T,U);return T}return U};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(N){E=function(X,W){var U=W||[];if(H.call(X)==="[object Array]"){Array.prototype.push.apply(U,X)}else{if(typeof X.length==="number"){for(var V=0,T=X.length;V";var T=document.documentElement;T.insertBefore(U,T.firstChild);if(!!document.getElementById(V)){I.find.ID=function(X,Y,Z){if(typeof Y.getElementById!=="undefined"&&!Z){var W=Y.getElementById(X[1]);return W?W.id===X[1]||typeof W.getAttributeNode!=="undefined"&&W.getAttributeNode("id").nodeValue===X[1]?[W]:g:[]}};I.filter.ID=function(Y,W){var X=typeof Y.getAttributeNode!=="undefined"&&Y.getAttributeNode("id");return Y.nodeType===1&&X&&X.nodeValue===W}}T.removeChild(U)})();(function(){var T=document.createElement("div");T.appendChild(document.createComment(""));if(T.getElementsByTagName("*").length>0){I.find.TAG=function(U,Y){var X=Y.getElementsByTagName(U[1]);if(U[1]==="*"){var W=[];for(var V=0;X[V];V++){if(X[V].nodeType===1){W.push(X[V])}}X=W}return X}}T.innerHTML="";if(T.firstChild&&typeof T.firstChild.getAttribute!=="undefined"&&T.firstChild.getAttribute("href")!=="#"){I.attrHandle.href=function(U){return U.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var T=F,U=document.createElement("div");U.innerHTML="

";if(U.querySelectorAll&&U.querySelectorAll(".TEST").length===0){return}F=function(Y,X,V,W){X=X||document;if(!W&&X.nodeType===9&&!Q(X)){try{return E(X.querySelectorAll(Y),V)}catch(Z){}}return T(Y,X,V,W)};F.find=T.find;F.filter=T.filter;F.selectors=T.selectors;F.matches=T.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var T=document.createElement("div");T.innerHTML="
";if(T.getElementsByClassName("e").length===0){return}T.lastChild.className="e";if(T.getElementsByClassName("e").length===1){return}I.order.splice(1,0,"CLASS");I.find.CLASS=function(U,V,W){if(typeof V.getElementsByClassName!=="undefined"&&!W){return V.getElementsByClassName(U[1])}}})()}function P(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W0){X=T;break}}}T=T[U]}ad[W]=X}}}var K=document.compareDocumentPosition?function(U,T){return U.compareDocumentPosition(T)&16}:function(U,T){return U!==T&&(U.contains?U.contains(T):true)};var Q=function(T){return T.nodeType===9&&T.documentElement.nodeName!=="HTML"||!!T.ownerDocument&&Q(T.ownerDocument)};var J=function(T,aa){var W=[],X="",Y,V=aa.nodeType?[aa]:aa;while((Y=I.match.PSEUDO.exec(T))){X+=Y[0];T=T.replace(I.match.PSEUDO,"")}T=I.relative[T]?T+"*":T;for(var Z=0,U=V.length;Z0||T.offsetHeight>0};F.selectors.filters.animated=function(T){return o.grep(o.timers,function(U){return T===U.elem}).length};o.multiFilter=function(V,T,U){if(U){V=":not("+V+")"}return F.matches(V,T)};o.dir=function(V,U){var T=[],W=V[U];while(W&&W!=document){if(W.nodeType==1){T.push(W)}W=W[U]}return T};o.nth=function(X,T,V,W){T=T||1;var U=0;for(;X;X=X[V]){if(X.nodeType==1&&++U==T){break}}return X};o.sibling=function(V,U){var T=[];for(;V;V=V.nextSibling){if(V.nodeType==1&&V!=U){T.push(V)}}return T};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var G=H;H=this.proxy(G);H.data=K}var E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",function(){return typeof o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var Q=O.split(".");O=Q.shift();var N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var P in G[O]){if(N.test(G[O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete G[O]}}})}for(F in G){break}if(!F){var L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var G=I.type||I;if(!E){I=typeof I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);K.currentTarget=this;var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(F=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("
").append(M.responseText.replace(//g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password|search/i.test(this.type))}).map(function(E,F){var G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return o.get(E,null,F,"script")},getJSON:function(E,F,G){return o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var E=e();var U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?"&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var H=document.getElementsByTagName("head")[0];var T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();T.onload=T.onreadystatechange=null;H.removeChild(T)}}}H.appendChild(T);return g}var K=false;var J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.dataType]+", */*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.async){N()}function I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return !F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof I==="string"){if(H=="script"){o.globalEval(I)}if(H=="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var F in E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return G.join("&").replace(/%20/g,"+")}});var m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var H=0,F=this.length;H").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove();m[G]=K}o.data(this[H],"olddisplay",K)}}for(var H=0,F=this.length;H=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,duration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return E},easing:{linear:function(G,H,E,F){return E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},custom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var E=this;function F(J){return E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)&&!n){n=setInterval(function(){var K=o.timers;for(var J=0;J=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var E=true;for(var F in this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var I in this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return false}else{var J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='
';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E in M){F.style[E]=M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var I=0,H=0,F;if(this[0]){var G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j(G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return null}return H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(I,G){var E=I?"Left":"Top",H=I?"Right":"Bottom",F=G.toLowerCase();o.fn["inner"+G]=function(){return this[0]?o.css(this[0],F,false,"padding"):null};o.fn["outer"+G]=function(K){return this[0]?o.css(this[0],F,false,K?"margin":"border"):null};var J=G.toLowerCase();o.fn[J]=function(K){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+G]||document.body["client"+G]:this[0]==document?Math.max(document.documentElement["client"+G],document.body["scroll"+G],document.documentElement["scroll"+G],document.body["offset"+G],document.documentElement["offset"+G]):K===g?(this.length?o.css(this[0],J):null):this.css(J,typeof K==="string"?K:K+"px")}})})(); \ No newline at end of file diff --git a/console-proxy/libexec/console-proxy-runner.in b/console-proxy/libexec/console-proxy-runner.in new file mode 100755 index 00000000000..4c01f7070d9 --- /dev/null +++ b/console-proxy/libexec/console-proxy-runner.in @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +#run.sh runs the agent client. + +cd `dirname "$0"` + +SYSTEMJARS="@SYSTEMJARS@" +SCP=$(build-classpath $SYSTEMJARS) ; if [ $? != 0 ] ; then SCP="@SYSTEMCLASSPATH@" ; fi +DCP="@DEPSCLASSPATH@" +ACP="@AGENTCLASSPATH@" +export CLASSPATH=$SCP:$DCP:$ACP:@CPSYSCONFDIR@ +for jarfile in "@PREMIUMJAVADIR@"/* ; do + if [ ! -e "$jarfile" ] ; then continue ; fi + CLASSPATH=$jarfile:$CLASSPATH +done +for plugin in "@PLUGINJAVADIR@"/* ; do + if [ ! -e "$plugin" ] ; then continue ; fi + CLASSPATH=$plugin:$CLASSPATH +done +export CLASSPATH + +set -e +cd "@CPLIBDIR@" +echo Current directory is "$PWD" +echo CLASSPATH to run the console proxy: "$CLASSPATH" + +export PATH=/sbin:/usr/sbin:"$PATH" +SERVICEARGS= +for x in private public ; do + configuration=`grep -q "^$x.network.device" "@CPSYSCONFDIR@"/agent.properties || true` + if [ -n "$CONFIGURATION" ] ; then + echo "Using manually-configured network device $CONFIGURATION" + else + defaultroute=`ip route | grep ^default | cut -d ' ' -f 5` + test -n "$defaultroute" + echo "Using auto-discovered network device $defaultroute which is the default route" + SERVICEARGS="$SERVICEARGS $x.network.device="$defaultroute + fi +done + +function termagent() { + if [ "$agentpid" != "" ] ; then + echo Killing VMOps Console Proxy "(PID $agentpid)" with SIGTERM >&2 + kill -TERM $agentpid + echo Waiting for agent to exit >&2 + wait $agentpid + ex=$? + echo Agent exited with return code $ex >&2 + else + echo Agent PID is unknown >&2 + fi +} + +trap termagent TERM +while true ; do + java -Xms128M -Xmx384M -cp "$CLASSPATH" "$@" com.cloud.agent.AgentShell $SERVICEARGS & + agentpid=$! + echo "Console Proxy started. PID: $!" >&2 + wait $agentpid + ex=$? + if [ $ex -gt 128 ]; then + echo "wait on console proxy process interrupted by SIGTERM" >&2 + exit $ex + fi + echo "Console proxy exited with return code $ex" >&2 + if [ $ex -eq 0 ] || [ $ex -eq 1 ] || [ $ex -eq 66 ] || [ $ex -gt 128 ]; then + echo "Exiting..." > /dev/stderr + exit $ex + fi + echo "Restarting console proxy..." > /dev/stderr + sleep 1 +done diff --git a/console-proxy/scripts/config_auth.sh b/console-proxy/scripts/config_auth.sh new file mode 100755 index 00000000000..893920d2be2 --- /dev/null +++ b/console-proxy/scripts/config_auth.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +BASE_DIR="/var/www/html/copy/template/" +HTACCESS="$BASE_DIR/.htaccess" +PASSWDFILE="/etc/httpd/.htpasswd" + +config_htaccess() { + mkdir -p $BASE_DIR + result=$? + echo "Options -Indexes" > $HTACCESS + let "result=$result+$?" + echo "AuthType Basic" >> $HTACCESS + let "result=$result+$?" + echo "AuthName \"Authentication Required\"" >> $HTACCESS + let "result=$result+$?" + echo "AuthUserFile \"$PASSWDFILE\"" >> $HTACCESS + let "result=$result+$?" + echo "Require valid-user" >> $HTACCESS + let "result=$result+$?" + return $result +} + +write_passwd() { + local user=$1 + local passwd=$2 + htpasswd -bc $PASSWDFILE $user $passwd + return $? +} + +if [ $# -ne 2 ] ; then + echo $"Usage: `basename $0` username password " + exit 0 +fi + +write_passwd $1 $2 +if [ $? -ne 0 ] +then + echo "Failed to update password" + exit 2 +fi + +config_htaccess +exit $? diff --git a/console-proxy/scripts/config_ssl.sh b/console-proxy/scripts/config_ssl.sh new file mode 100755 index 00000000000..a3be8d32dff --- /dev/null +++ b/console-proxy/scripts/config_ssl.sh @@ -0,0 +1,40 @@ +#!/bin/bash + + +config_httpd_conf() { + local ip=$1 + local srvr=$2 + cp -f /etc/httpd/conf/httpd.conf.orig /etc/httpd/conf/httpd.conf + sed -i -e "s/Listen.*:80$/Listen $ip:80/" /etc/httpd/conf/httpd.conf + echo " " >> /etc/httpd/conf/httpd.conf + echo " DocumentRoot /var/www/html/" >> /etc/httpd/conf/httpd.conf + echo " ServerName $srvr" >> /etc/httpd/conf/httpd.conf + echo " SSLEngine on" >> /etc/httpd/conf/httpd.conf + echo " SSLCertificateFile /etc/httpd/ssl/certs/realhostip.crt" >> /etc/httpd/conf/httpd.conf + echo " SSLCertificateKeyFile /etc/httpd/ssl/keys/realhostip.key" >> /etc/httpd/conf/httpd.conf + echo "" >> /etc/httpd/conf/httpd.conf +} + +copy_certs() { + local certdir=$(dirname $0)/certs + local mydir=$(dirname $0) + if [ -d $certdir ] && [ -f $certdir/realhostip.key ] && [ -f $certdir/realhostip.crt ] ; then + mkdir -p /etc/httpd/ssl/keys && mkdir -p /etc/httpd/ssl/certs && cp $certdir/realhostip.key /etc/httpd/ssl/keys && cp $certdir/realhostip.crt /etc/httpd/ssl/certs + return $? + fi + return 1 +} + +if [ $# -ne 2 ] ; then + echo $"Usage: `basename $0` ipaddr servername " + exit 0 +fi + +copy_certs +if [ $? -ne 0 ] +then + echo "Failed to copy certificates" + exit 2 +fi + +config_httpd_conf $1 $2 diff --git a/console-proxy/scripts/run-proxy.sh b/console-proxy/scripts/run-proxy.sh new file mode 100644 index 00000000000..b6c2e88a4e2 --- /dev/null +++ b/console-proxy/scripts/run-proxy.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +#run.sh runs the console proxy. + +# make sure we delete the old files from the original template +rm console-proxy.jar +rm console-common.jar +rm conf/cloud.properties + +CP=./:./conf +for file in *.jar +do + CP=${CP}:$file +done + +#CMDLINE=$(cat /proc/cmdline) +#for i in $CMDLINE +# do +# KEY=$(echo $i | cut -d= -f1) +# VALUE=$(echo $i | cut -d= -f2) +# case $KEY in +# mgmt_host) +# MGMT_HOST=$VALUE +# ;; +# esac +# done + +java -mx700m -cp $CP:./conf com.cloud.consoleproxy.ConsoleProxy $@ diff --git a/console-proxy/scripts/run.bat b/console-proxy/scripts/run.bat new file mode 100644 index 00000000000..a24d232bb71 --- /dev/null +++ b/console-proxy/scripts/run.bat @@ -0,0 +1 @@ +java -mx700m -cp cloud-console-proxy.jar;cloud-console-proxy-premium.jar;cloud-console-common.jar;log4j-1.2.15.jar;apache-log4j-extras-1.0.jar;gson-1.3.jar;commons-logging-1.1.1.jar;.;.\conf; com.cloud.consoleproxy.ConsoleProxy %* diff --git a/console-proxy/scripts/run.sh b/console-proxy/scripts/run.sh new file mode 100755 index 00000000000..e90005d2ebb --- /dev/null +++ b/console-proxy/scripts/run.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +#run.sh runs the console proxy. + +# make sure we delete the old files from the original template +rm console-proxy.jar +rm console-common.jar +rm conf/cloud.properties + +set -x + +CP=./:./conf +for file in *.jar +do + CP=${CP}:$file +done +keyvalues= +if [ -f /mnt/cmdline ] +then + CMDLINE=$(cat /mnt/cmdline) +else + CMDLINE=$(cat /proc/cmdline) +fi +#CMDLINE="graphical utf8 eth0ip=0.0.0.0 eth0mask=255.255.255.0 eth1ip=192.168.140.40 eth1mask=255.255.255.0 eth2ip=172.24.0.50 eth2mask=255.255.0.0 gateway=172.24.0.1 dns1=72.52.126.11 template=domP dns2=72.52.126.12 host=192.168.1.142 port=8250 mgmtcidr=192.168.1.0/24 localgw=192.168.140.1 zone=5 pod=5" +for i in $CMDLINE + do + KEY=$(echo $i | cut -s -d= -f1) + VALUE=$(echo $i | cut -s -d= -f2) + [ "$KEY" == "" ] && continue + case $KEY in + *) + keyvalues="${keyvalues} $KEY=$VALUE" + esac + done + +tot_mem_k=$(cat /proc/meminfo | grep MemTotal | awk '{print $2}') +let "tot_mem_m=tot_mem_k>>10" +let "eightypcnt=$tot_mem_m*8/10" +let "maxmem=$tot_mem_m-80" + +if [ $maxmem -gt $eightypcnt ] +then + maxmem=$eightypcnt +fi + +EXTRA= +if [ -f certs/realhostip.keystore ] +then + EXTRA="-Djavax.net.ssl.trustStore=$(dirname $0)/certs/realhostip.keystore -Djavax.net.ssl.trustStorePassword=vmops.com" +fi + +java -mx${maxmem}m ${EXTRA} -cp $CP com.cloud.agent.AgentShell $keyvalues $@ diff --git a/console-proxy/scripts/ssvm-check.sh b/console-proxy/scripts/ssvm-check.sh new file mode 100644 index 00000000000..b4e8cb4d3bb --- /dev/null +++ b/console-proxy/scripts/ssvm-check.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# +# Health check script for the Secondary Storage VM +# + +# /proc/cmdline can have a different number of arguments depending on whether or not a second +# DNS server is specified. + +grep dns2 /proc/cmdline 1> /dev/null 2>&1 +HAVEDNS2=$? + +# ping dns server +echo ================================================ +DNSSERVER=`egrep '^nameserver' /etc/resolv.conf | awk '{print $2}'| head -1` +echo "First DNS server is " $DNSSERVER +ping -c 2 -w 2 $DNSSERVER +if [ $? -eq 0 ] +then + echo "Good: Can ping DNS server" +else + echo "WARNING: cannot ping DNS server" + echo "route follows" + route -n +fi + + +# check dns resolve +echo ================================================ +nslookup download.cloud.com 1> /tmp/dns 2>&1 +grep 'no servers could' /tmp/dns 1> /dev/null 2>&1 +if [ $? -eq 0 ] +then + echo "ERROR: DNS not resolving download.cloud.com" + echo resolv.conf follows + cat /etc/resolv.conf + exit 2 +else + echo "Good: DNS resolves download.cloud.com" +fi + + +# check to see if we have the NFS volume mounted +echo ================================================ +mount|grep -v sunrpc|grep nfs 1> /dev/null 2>&1 +if [ $? -eq 0 ] +then + echo "NFS is currently mounted" + + # check for write access + MOUNTPT=`mount|grep -v sunrpc|grep nfs| awk '{print $3}'` + echo Mount point is $MOUNTPT + touch $MOUNTPT/foo + if [ $? -eq 0 ] + then + echo "Good: Can write to mount point" + rm $MOUNTPT/foo + else + echo "ERROR: Cannot write to mount point" + echo "You need to export with norootsquash" + fi +else + echo "ERROR: NFS is not currently mounted" + echo "Try manually mounting from inside the VM" + if [ $HAVEDNS2 -eq 0 ] + then + NFSSERVER=`awk '{print $24}' /proc/cmdline|awk -F= '{print $2}'|awk -F: '{print $1}'` + else + NFSSERVER=`awk '{print $23}' /proc/cmdline|awk -F= '{print $2}'|awk -F: '{print $1}'` + fi + echo "NFS server is " $NFSSERVER + ping -c 2 -w 2 $NFSSERVER + if [ $? -eq 0 ] + then + echo "Good: Can ping NFS server" + else + echo "WARNING: cannot ping NFS server" + echo routing table follows + route -n + fi +fi + + +# check for connectivity to the management server +echo ================================================ +if [ $HAVEDNS2 -eq 0 ] +then + MGMTSERVER=`awk '{print $21}' /proc/cmdline | awk -F= '{print $2}'` +else + MGMTSERVER=`awk '{print $20}' /proc/cmdline | awk -F= '{print $2}'` +fi +echo Management server is $MGMTSERVER. Checking connectivity. +socatout=$(echo | socat - TCP:$MGMTSERVER:8250,connect-timeout=3 2>&1) +if [ $? -eq 0 ] +then + echo "Good: Can connect to management server port 8250" +else + echo "ERROR: Cannot connect to $MGMTSERVER port 8250" + echo $socatout + exit 4 +fi + + +# check for the java process running +echo ================================================ +ps -eaf|grep -v grep|grep java 1> /dev/null 2>&1 +if [ $? -eq 0 ] +then + echo "Good: Java process is running" +else + echo "ERROR: Java process not running. Try restarting the SSVM." + exit 3 +fi + +echo ================================================ +echo Tests Complete. Look for ERROR or WARNING above. + +exit 0 diff --git a/console-proxy/src/com/cloud/consoleproxy/AjaxFIFOImageCache.java b/console-proxy/src/com/cloud/consoleproxy/AjaxFIFOImageCache.java new file mode 100644 index 00000000000..0e8fcacce34 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/AjaxFIFOImageCache.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.cloud.console.Logger; + +public class AjaxFIFOImageCache { + private static final Logger s_logger = Logger.getLogger(AjaxFIFOImageCache.class); + + private List fifoQueue; + private Map cache; + private int cacheSize; + private int nextKey = 1; + + public AjaxFIFOImageCache(int cacheSize) { + this.cacheSize = cacheSize; + fifoQueue = new ArrayList(); + cache = new HashMap(); + } + + public synchronized void clear() { + fifoQueue.clear(); + cache.clear(); + } + + public synchronized int putImage(byte[] image) { + while(cache.size() >= cacheSize) { + Integer keyToRemove = fifoQueue.remove(0); + cache.remove(keyToRemove); + + if(s_logger.isTraceEnabled()) + s_logger.trace("Remove image from cache, key: " + keyToRemove); + } + + int key = getNextKey(); + + if(s_logger.isTraceEnabled()) + s_logger.trace("Add image to cache, key: " + key); + + cache.put(key, image); + fifoQueue.add(key); + return key; + } + + public synchronized byte[] getImage(int key) { + if(cache.containsKey(key)) { + if(s_logger.isTraceEnabled()) + s_logger.trace("Retrieve image from cache, key: " + key); + + return cache.get(key); + } + + if(s_logger.isTraceEnabled()) + s_logger.trace("Image is no long in cache, key: " + key); + return null; + } + + public synchronized int getNextKey() { + return nextKey++; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java new file mode 100644 index 00000000000..a1a1b35e915 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxy.java @@ -0,0 +1,682 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +// +package com.cloud.consoleproxy; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +import javax.net.ssl.SSLServerSocket; + +import org.apache.log4j.xml.DOMConfigurator; + +import com.cloud.console.AuthenticationException; +import com.cloud.console.Logger; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.sun.net.httpserver.HttpServer; + +public class ConsoleProxy { + private static final Logger s_logger = Logger.getLogger(ConsoleProxy.class); +/* + private static final int MAX_STRONG_TEMPLATE_CACHE_SIZE = 100; + private static final int MAX_SOFT_TEMPLATE_CACHE_SIZE = 100; +*/ + + public static final int KEYBOARD_RAW = 0; + public static final int KEYBOARD_COOKED = 1; + + public static Object context; + public static Method authMethod; + public static Method reportMethod; + public static Method ensureRouteMethod; + + static Hashtable connectionMap = new Hashtable(); + static int tcpListenPort = 5999; + static int httpListenPort = 80; + static int httpCmdListenPort = 8001; + static String jarDir = "./applet/"; + static boolean compressServerMessage = true; + static int viewerLinger = 180; + static int reconnectMaxRetry = 5; + static int readTimeoutSeconds = 90; + static int keyboardType = KEYBOARD_RAW; + static String factoryClzName; + static boolean standaloneStart = false; + + private static void configLog4j() { + URL configUrl = System.class.getResource("/conf/log4j-cloud.xml"); + if(configUrl == null) + configUrl = System.class.getClassLoader().getSystemResource("log4j-cloud.xml"); + + if(configUrl == null) + configUrl = System.class.getClassLoader().getSystemResource("conf/log4j-cloud.xml"); + + if(configUrl != null) { + try { + System.out.println("Configure log4j using " + configUrl.toURI().toString()); + } catch (URISyntaxException e1) { + e1.printStackTrace(); + } + + try { + File file = new File(configUrl.toURI()); + + System.out.println("Log4j configuration from : " + file.getAbsolutePath()); + DOMConfigurator.configureAndWatch(file.getAbsolutePath(), 10000); + } catch (URISyntaxException e) { + System.out.println("Unable to convert log4j configuration Url to URI"); + } + // DOMConfigurator.configure(configUrl); + } else { + System.out.println("Configure log4j with default properties"); + } + } + + private static void configProxy(Properties conf) { + s_logger.info("Configure console proxy..."); + for(Object key : conf.keySet()) { + s_logger.info("Property " + (String)key + ": " + conf.getProperty((String)key)); + } + + String s = conf.getProperty("consoleproxy.tcpListenPort"); + if (s!=null) { + tcpListenPort = Integer.parseInt(s); + s_logger.info("Setting tcpListenPort=" + s); + } + + s = conf.getProperty("consoleproxy.httpListenPort"); + if (s!=null) { + httpListenPort = Integer.parseInt(s); + s_logger.info("Setting httpListenPort=" + s); + } + + s = conf.getProperty("premium"); + if(s != null && s.equalsIgnoreCase("true")) { + s_logger.info("Premium setting will override settings from consoleproxy.properties, listen at port 443"); + httpListenPort = 443; + factoryClzName = "com.cloud.consoleproxy.ConsoleProxySecureServerFactoryImpl"; + } else { + factoryClzName = ConsoleProxyBaseServerFactoryImpl.class.getName(); + } + + s = conf.getProperty("consoleproxy.httpCmdListenPort"); + if (s!=null) { + httpCmdListenPort = Integer.parseInt(s); + s_logger.info("Setting httpCmdListenPort=" + s); + } + s = conf.getProperty("consoleproxy.jarDir"); + if (s!=null) { + jarDir = s; + s_logger.info("Setting jarDir=" + s); + } + s = conf.getProperty("consoleproxy.viewerLinger"); + if (s!=null) { + viewerLinger = Integer.parseInt(s); + s_logger.info("Setting viewerLinger=" + s); + } + s = conf.getProperty("consoleproxy.compressServerMessage"); + if (s!=null) { + compressServerMessage = Boolean.parseBoolean(s); + s_logger.info("Setting compressServerMessage=" + s); + } + + s = conf.getProperty("consoleproxy.reconnectMaxRetry"); + if (s!=null) { + reconnectMaxRetry = Integer.parseInt(s); + s_logger.info("Setting reconnectMaxRetry=" + reconnectMaxRetry); + } + + s = conf.getProperty("consoleproxy.readTimeoutSeconds"); + if (s!=null) { + readTimeoutSeconds = Integer.parseInt(s); + s_logger.info("Setting readTimeoutSeconds=" + readTimeoutSeconds); + } + } + + public static ConsoleProxyServerFactory getHttpServerFactory() { + try { + Class clz = Class.forName(factoryClzName); + try { + return (ConsoleProxyServerFactory)clz.newInstance(); + + } catch (InstantiationException e) { + s_logger.error(e.getMessage(), e); + return null; + } catch (IllegalAccessException e) { + s_logger.error(e.getMessage(), e); + return null; + } + } catch (ClassNotFoundException e) { + s_logger.warn("Unable to find http server factory class: " + factoryClzName); + return new ConsoleProxyBaseServerFactoryImpl(); + } + } + + public static boolean authenticateConsoleAccess(String vmId, String sid) { + if(standaloneStart) + return true; + + if(authMethod != null) { + Object result; + try { + result = authMethod.invoke(ConsoleProxy.context, vmId, sid); + } catch (IllegalAccessException e) { + s_logger.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + vmId, e); + return false; + } catch (InvocationTargetException e) { + s_logger.error("Unable to invoke authenticateConsoleAccess due to InvocationTargetException " + " for vm: " + vmId, e); + return false; + } + + if(result != null && result instanceof Boolean) { + return ((Boolean)result).booleanValue(); + } else { + s_logger.error("Invalid authentication return object " + result + " for vm: " + vmId + ", decline the access"); + return false; + } + + } else { + s_logger.warn("Private channel towards management server is not setup. Switch to offline mode and allow access to vm: " + vmId); + return true; + } + } + + public static void reportLoadInfo(String gsonLoadInfo) { + if(reportMethod != null) { + try { + reportMethod.invoke(ConsoleProxy.context, gsonLoadInfo); + } catch (IllegalAccessException e) { + s_logger.error("Unable to invoke reportLoadInfo due to " + e.getMessage()); + } catch (InvocationTargetException e) { + s_logger.error("Unable to invoke reportLoadInfo due to " + e.getMessage()); + } + } else { + s_logger.warn("Private channel towards management server is not setup. Switch to offline mode and ignore load report"); + } + } + + public static void ensureRoute(String address) { + if(ensureRouteMethod != null) { + try { + ensureRouteMethod.invoke(ConsoleProxy.context, address); + } catch (IllegalAccessException e) { + s_logger.error("Unable to invoke ensureRoute due to " + e.getMessage()); + } catch (InvocationTargetException e) { + s_logger.error("Unable to invoke ensureRoute due to " + e.getMessage()); + } + } else { + s_logger.warn("Unable to find ensureRoute method, console proxy agent is not up to date"); + } + } + + public static void startWithContext(Properties conf, Object context) { + s_logger.info("Start console proxy with context"); + if(conf != null) { + for(Object key : conf.keySet()) { + s_logger.info("Context property " + (String)key + ": " + conf.getProperty((String)key)); + } + } + + configLog4j(); + Logger.setFactory(new ConsoleProxyLoggerFactory()); + + // Using reflection to setup private/secure communication channel towards management server + ConsoleProxy.context = context; + try { + Class contextClazz = Class.forName("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource"); + authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class); + reportMethod = contextClazz.getDeclaredMethod("reportLoadInfo", String.class); + ensureRouteMethod = contextClazz.getDeclaredMethod("ensureRoute", String.class); + } catch (SecurityException e) { + s_logger.error("Unable to setup private channel due to SecurityException", e); + } catch (NoSuchMethodException e) { + s_logger.error("Unable to setup private channel due to NoSuchMethodException", e); + } catch (IllegalArgumentException e) { + s_logger.error("Unable to setup private channel due to IllegalArgumentException", e); + } catch(ClassNotFoundException e) { + s_logger.error("Unable to setup private channel due to ClassNotFoundException", e); + } + + // merge properties from conf file + InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties"); + Properties props = new Properties(); + if (confs == null) { + s_logger.info("Can't load consoleproxy.properties from classpath, will use default configuration"); + } else { + try { + props.load(confs); + + for(Object key : props.keySet()) { + // give properties passed via context high priority, treat properties from consoleproxy.properties + // as default values + if(conf.get(key) == null) + conf.put(key, props.get(key)); + } + } catch (Exception e) { + s_logger.error(e.toString(), e); + } + } + + start(conf); + } + + public static void start(Properties conf) { + System.setProperty("java.awt.headless", "true"); + + configProxy(conf); + + ConsoleProxyServerFactory factory = getHttpServerFactory(); + if(factory == null) { + s_logger.error("Unable to load console proxy server factory"); + System.exit(1); + } + + if(httpListenPort != 0) { + startupHttpMain(); + } else { + s_logger.error("A valid HTTP server port is required to be specified, please check your consoleproxy.httpListenPort settings"); + System.exit(1); + } + + if(httpCmdListenPort > 0) { + startupHttpCmdPort(); + } else { + s_logger.info("HTTP command port is disabled"); + } + + ViewerGCThread cthread = new ViewerGCThread(connectionMap); + cthread.setName("Viewer GC Thread"); + cthread.start(); + + if(tcpListenPort > 0) { + SSLServerSocket srvSock = null; + try { + srvSock = factory.createSSLServerSocket(tcpListenPort); + s_logger.info("Listening for TCP on port " + tcpListenPort); + } catch (IOException ioe) { + s_logger.error(ioe.toString(), ioe); + System.exit(1); + } + + if(srvSock != null) { + while (true) { + Socket conn = null; + try { + conn = srvSock.accept(); + String srcinfo = conn.getInetAddress().getHostAddress() + ":" + conn.getPort(); + s_logger.info("Accepted connection from " + srcinfo); + conn.setSoLinger(false,0); + ConsoleProxyClientHandler worker = new ConsoleProxyClientHandler(conn); + worker.setName("Proxy Thread " + worker.getId() + " <" + srcinfo); + worker.start(); + } catch (IOException ioe2) { + s_logger.error(ioe2.toString(), ioe2); + try { + if (conn != null) { + conn.close(); + } + } catch (IOException ioe) {} + } catch (Throwable e) { + // Something really bad happened + // Terminate the program + s_logger.error(e.toString(), e); + System.exit(1); + } + } + } else { + s_logger.warn("TCP port is enabled in configuration but we are not able to instantiate server socket."); + } + } else { + s_logger.info("TCP port is disabled for applet viewers"); + } + } + + private static void startupHttpMain() { + try { + ConsoleProxyServerFactory factory = getHttpServerFactory(); + if(factory == null) { + s_logger.error("Unable to load HTTP server factory"); + System.exit(1); + } + + HttpServer server = factory.createHttpServerInstance(httpListenPort); + server.createContext("/getscreen", new ConsoleProxyThumbnailHandler()); + server.createContext("/getjar/", new ConsoleProxyJarHandler()); + server.createContext("/resource/", new ConsoleProxyResourceHandler()); + server.createContext("/ajax", new ConsoleProxyAjaxHandler()); + server.createContext("/ajaximg", new ConsoleProxyAjaxImageHandler()); + server.setExecutor(new ThreadExecutor()); // creates a default executor + server.start(); + } catch(Exception e) { + s_logger.error(e.getMessage(), e); + System.exit(1); + } + } + + private static void startupHttpCmdPort() { + try { + s_logger.info("Listening for HTTP CMDs on port " + httpCmdListenPort); + HttpServer cmdServer = HttpServer.create(new InetSocketAddress(httpCmdListenPort), 2); + cmdServer.createContext("/cmd", new ConsoleProxyCmdHandler()); + cmdServer.setExecutor(new ThreadExecutor()); // creates a default executor + cmdServer.start(); + } catch(Exception e) { + s_logger.error(e.getMessage(), e); + System.exit(1); + } + } + + public static void main(String[] argv) { + standaloneStart = true; + configLog4j(); + Logger.setFactory(new ConsoleProxyLoggerFactory()); + + InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties"); + Properties conf = new Properties(); + if (confs == null) { + s_logger.info("Can't load consoleproxy.properties from classpath, will use default configuration"); + } else { + try { + conf.load(confs); + } catch (Exception e) { + s_logger.error(e.toString(), e); + } + } + start(conf); + } + + static ConsoleProxyViewer createViewer() { + ConsoleProxyViewer viewer = new ConsoleProxyViewer(); + viewer.compressServerMessage = compressServerMessage; + return viewer; + } + + static void initViewer(ConsoleProxyViewer viewer, String host, int port, String tag, String sid) throws AuthenticationException { + ConsoleProxyViewer.authenticationExternally(tag, sid); + + viewer.host = host; + viewer.port = port; + viewer.tag = tag; + viewer.passwordParam = sid; + + viewer.init(); + } + + static ConsoleProxyViewer getVncViewer(String host, int port, String sid, String tag) throws Exception { + ConsoleProxyViewer viewer = null; + + boolean reportLoadChange = false; + synchronized (connectionMap) { + viewer = connectionMap.get(host + ":" + port); + if (viewer == null) { + viewer = createViewer(); + initViewer(viewer, host, port, tag, sid); + connectionMap.put(host + ":" + port, viewer); + s_logger.info("Added viewer object " + viewer); + + reportLoadChange = true; + } else if (!viewer.rfbThread.isAlive()) { + s_logger.info("The rfb thread died, reinitializing the viewer " + + viewer); + initViewer(viewer, host, port, tag, sid); + + reportLoadChange = true; + } else if (!sid.equals(viewer.passwordParam)) { + s_logger.warn("Bad sid detected(VNC port may be reused). sid in session: " + viewer.passwordParam + ", sid in request: " + sid); + initViewer(viewer, host, port, tag, sid); + + reportLoadChange = true; + + /* + throw new AuthenticationException ("Cannot use the existing viewer " + + viewer + ": bad sid"); + */ + } + } + + if(viewer != null) { + if (viewer.status == ConsoleProxyViewer.STATUS_NORMAL_OPERATION) { + // Do not update lastUsedTime if the viewer is in the process of starting up + // or if it failed to authenticate. + viewer.lastUsedTime = System.currentTimeMillis(); + } + } + + if(reportLoadChange) { + ConsoleProxyStatus status = new ConsoleProxyStatus(); + status.setConnections(ConsoleProxy.connectionMap); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String loadInfo = gson.toJson(status); + + ConsoleProxy.reportLoadInfo(loadInfo); + if(s_logger.isDebugEnabled()) + s_logger.debug("Report load change : " + loadInfo); + } + + return viewer; + } + + static ConsoleProxyViewer getAjaxVncViewer(String host, int port, String sid, String tag) throws Exception { + boolean reportLoadChange = false; + synchronized (connectionMap) { + ConsoleProxyViewer viewer = connectionMap.get(host + ":" + port); +// s_logger.info("view lookup " + host + ":" + port + " = " + viewer); + + if (viewer == null) { + viewer = createViewer(); + viewer.ajaxViewer = true; + + initViewer(viewer, host, port, tag, sid); + connectionMap.put(host + ":" + port, viewer); + s_logger.info("Added viewer object " + viewer); + reportLoadChange = true; + } else if (!viewer.rfbThread.isAlive()) { + s_logger.info("The rfb thread died, reinitializing the viewer " + + viewer); + initViewer(viewer, host, port, tag, sid); + reportLoadChange = true; + } else if (!sid.equals(viewer.passwordParam)) { + s_logger.warn("Bad sid detected(VNC port may be reused). sid in session: " + viewer.passwordParam + ", sid in request: " + sid); + initViewer(viewer, host, port, tag, sid); + reportLoadChange = true; + + /* + throw new AuthenticationException ("Cannot use the existing viewer " + + viewer + ": bad sid"); + */ + } + if (viewer.status == ConsoleProxyViewer.STATUS_NORMAL_OPERATION) { + // Do not update lastUsedTime if the viewer is in the process of starting up + // or if it failed to authenticate. + viewer.lastUsedTime = System.currentTimeMillis(); + } + + if(reportLoadChange) { + ConsoleProxyStatus status = new ConsoleProxyStatus(); + status.setConnections(ConsoleProxy.connectionMap); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String loadInfo = gson.toJson(status); + + ConsoleProxy.reportLoadInfo(loadInfo); + if(s_logger.isDebugEnabled()) + s_logger.debug("Report load change : " + loadInfo); + } + return viewer; + } + } + + public static void removeViewer(ConsoleProxyViewer viewer) { + synchronized (connectionMap) { + for(Map.Entry entry : connectionMap.entrySet()) { + if(entry.getValue() == viewer) { + connectionMap.remove(entry.getKey()); + return; + } + } + } + } + + static void waitForViewerToStart(ConsoleProxyViewer viewer) throws Exception { + if (viewer.status == ConsoleProxyViewer.STATUS_NORMAL_OPERATION) { + return; + } + + Long startTime = System.currentTimeMillis(); + int delay = 500; + + while (System.currentTimeMillis() < startTime + 30000 && + viewer.status != ConsoleProxyViewer.STATUS_NORMAL_OPERATION) { + if (viewer.status == ConsoleProxyViewer.STATUS_AUTHENTICATION_FAILURE) { + throw new Exception ("Authentication failure"); + } + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + // ignore + } + delay = (int)(delay * 1.5); + } + + if (viewer.status != ConsoleProxyViewer.STATUS_NORMAL_OPERATION) { + throw new Exception ("Cannot start VncViewer"); + } + + s_logger.info("Waited " + + (System.currentTimeMillis() - startTime) + "ms for VncViewer to start"); + } + + static class ThreadExecutor implements Executor { + public void execute(Runnable r) { + new Thread(r).start(); + } + } + + static class ViewerGCThread extends Thread { + Hashtable connMap; + long lastLogScan = 0L; + + public ViewerGCThread(Hashtable connMap) { + this.connMap = connMap; + } + + private void cleanupLogging() { + if(lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000) + return; + lastLogScan = System.currentTimeMillis(); + + File logDir = new File("./logs"); + File files[] = logDir.listFiles(); + if(files != null) { + for(File file : files) { + if(System.currentTimeMillis() - file.lastModified() >= 86400000L) { + try { + file.delete(); + } catch(Throwable e) { + } + } + } + } + } + + @Override + public void run() { + while (true) { + cleanupLogging(); + + s_logger.info("connMap=" + connMap); + Enumeration e = connMap.keys(); + while (e.hasMoreElements()) { + String key; + ConsoleProxyViewer viewer; + + synchronized (connMap) { + key = e.nextElement(); + viewer = connMap.get(key); + } + + long seconds_unused = + (System.currentTimeMillis() - viewer.lastUsedTime) / 1000; + + if (seconds_unused > viewerLinger / 2 && viewer.clientStream != null) { + s_logger.info("Pinging client for " + viewer + + " which has not been used for " + seconds_unused + "sec"); + byte[] bs = new byte[2]; + bs[0] = (byte)250; + bs[1] = 3; + viewer.writeToClientStream(bs); + } + + if (seconds_unused < viewerLinger) { + continue; + } + + synchronized (connMap) { + connMap.remove(key); + } + // close the server connection + s_logger.info("Dropping " + viewer + + " which has not been used for " + + seconds_unused + " seconds"); + viewer.dropMe = true; + synchronized (viewer) { + if (viewer.clientStream != null) { + try { + viewer.clientStream.close(); + } catch (IOException ioe) { + // ignored + } + viewer.clientStream = null; + viewer.clientStreamInfo = null; + } + if (viewer.rfb != null) { + viewer.rfb.close(); + } + } + + // report load change for removal of the viewer + ConsoleProxyStatus status = new ConsoleProxyStatus(); + status.setConnections(ConsoleProxy.connectionMap); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String loadInfo = gson.toJson(status); + + ConsoleProxy.reportLoadInfo(loadInfo); + if(s_logger.isDebugEnabled()) + s_logger.debug("Report load change : " + loadInfo); + } + try { + Thread.sleep(30000); + } catch (InterruptedException exp) { + // Ignore + } + } + } + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java new file mode 100644 index 00000000000..0324988c64b --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java @@ -0,0 +1,415 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Map; + +import com.cloud.console.Logger; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public class ConsoleProxyAjaxHandler implements HttpHandler { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyAjaxHandler.class); + + public ConsoleProxyAjaxHandler() { + } + + public void handle(HttpExchange t) throws IOException { + try { + if(s_logger.isTraceEnabled()) + s_logger.trace("AjaxHandler " + t.getRequestURI()); + + long startTick = System.currentTimeMillis(); + + doHandle(t); + + if(s_logger.isTraceEnabled()) + s_logger.trace(t.getRequestURI() + " process time " + (System.currentTimeMillis() - startTick) + " ms"); + } catch (IOException e) { + throw e; + } catch (IllegalArgumentException e) { + s_logger.warn("Exception, ", e); + t.sendResponseHeaders(400, -1); // bad request + } catch(Throwable e) { + s_logger.error("Unexpected exception, ", e); + t.sendResponseHeaders(500, -1); // server error + } finally { + t.close(); + } + } + + private void doHandle(HttpExchange t) throws Exception, IllegalArgumentException { + String queries = t.getRequestURI().getQuery(); + if(s_logger.isTraceEnabled()) + s_logger.trace("Handle AJAX request: " + queries); + + Map queryMap = getQueryMap(queries); + + String host = queryMap.get("host"); + String portStr = queryMap.get("port"); + String sid = queryMap.get("sid"); + String tag = queryMap.get("tag"); + String ajaxSessionIdStr = queryMap.get("sess"); + String eventStr = queryMap.get("event"); + if(tag == null) + tag = ""; + + long ajaxSessionId = 0; + int event = 0; + + int port; + + if(host == null || portStr == null || sid == null) + throw new IllegalArgumentException(); + + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + portStr); + throw new IllegalArgumentException(e); + } + + if(ajaxSessionIdStr != null) { + try { + ajaxSessionId = Long.parseLong(ajaxSessionIdStr); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + ajaxSessionIdStr); + throw new IllegalArgumentException(e); + } + } + + if(eventStr != null) { + try { + event = Integer.parseInt(eventStr); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + eventStr); + throw new IllegalArgumentException(e); + } + } + + ConsoleProxyViewer viewer = null; + try { + viewer = ConsoleProxy.getAjaxVncViewer(host, port, sid, tag); + } catch(Exception e) { + +/* + StringWriter writer = new StringWriter(); + try { + ConsoleProxy.processTemplate("viewer-bad-sid.ftl", null, writer); + } catch (IOException ex) { + s_logger.warn("Unexpected exception in processing template.", e); + } catch (TemplateException ex) { + s_logger.warn("Unexpected exception in processing template.", e); + } + StringBuffer sb = writer.getBuffer(); +*/ + s_logger.warn("Failed to create viwer due to " + e.getMessage(), e); + + String[] content = new String[] { + "", + "
", + "

Access is denied for the console session. Please close the window and retry again

", + "
" + }; + + StringBuffer sb = new StringBuffer(); + for(int i = 0; i < content.length; i++) + sb.append(content[i]); + + sendResponse(t, "text/html", sb.toString()); + return; + } + + if(event != 0) { + if(ajaxSessionId != 0 && ajaxSessionId == viewer.getAjaxSessionId()) { + if(event == 7) { + // client send over an event bag + InputStream is = t.getRequestBody(); + handleClientEventBag(viewer, convertStreamToString(is, true)); + } else { + handleClientEvent(viewer, event, queryMap); + } + sendResponse(t, "text/html", "OK"); + } else { + if(s_logger.isDebugEnabled()) + s_logger.debug("Ajax request comes from a different session, id in request: " + ajaxSessionId + ", id in viewer: " + viewer.getAjaxSessionId()); + + sendResponse(t, "text/html", "Invalid ajax client session id"); + } + } else { + if(ajaxSessionId != 0 && ajaxSessionId != viewer.getAjaxSessionId()) { + if(s_logger.isDebugEnabled()) + s_logger.debug("Ajax request comes from a different session, id in request: " + ajaxSessionId + ", id in viewer: " + viewer.getAjaxSessionId()); + handleClientKickoff(t, viewer); + } else if(ajaxSessionId == 0) { + if(s_logger.isDebugEnabled()) + s_logger.debug("Ajax request indicates a fresh client start"); + + String title = queryMap.get("t"); + handleClientStart(t, viewer, title != null ? title : ""); + } else { + + if(s_logger.isTraceEnabled()) + s_logger.trace("Ajax request indicates client update"); + + handleClientUpdate(t, viewer); + } + } + } + + public static Map getQueryMap(String query) { + String[] params = query.split("&"); + Map map = new HashMap(); + for (String param : params) { + String name = param.split("=")[0]; + String value = param.split("=")[1]; + map.put(name, value); + } + return map; + } + + private static String convertStreamToString(InputStream is, boolean closeStreamAfterRead) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line = null; + try { + while ((line = reader.readLine()) != null) { + sb.append(line + "\n"); + } + } catch (IOException e) { + s_logger.warn("Exception while reading request body: ", e); + } finally { + if(closeStreamAfterRead) { + try { + is.close(); + } catch (IOException e) { + } + } + } + return sb.toString(); + } + + private void sendResponse(HttpExchange t, String contentType, String response) throws IOException { + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", contentType); + + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + try { + os.write(response.getBytes()); + } finally { + os.close(); + } + } + + private void handleClientEventBag(ConsoleProxyViewer viewer, String requestData) { + if(s_logger.isTraceEnabled()) + s_logger.trace("Handle event bag, event bag: " + requestData); + + int start = requestData.indexOf("="); + if(start < 0) + start = 0; + else if(start > 0) + start++; + String data = URLDecoder.decode(requestData.substring(start)); + String[] tokens = data.split("\\|"); + if(tokens != null && tokens.length > 0) { + int count = 0; + try { + count = Integer.parseInt(tokens[0]); + int parsePos = 1; + int type, event, x, y, code, modifiers; + for(int i = 0; i < count; i++) { + type = Integer.parseInt(tokens[parsePos++]); + if(type == 1) { + // mouse event + event = Integer.parseInt(tokens[parsePos++]); + x = Integer.parseInt(tokens[parsePos++]); + y = Integer.parseInt(tokens[parsePos++]); + code = Integer.parseInt(tokens[parsePos++]); + modifiers = Integer.parseInt(tokens[parsePos++]); + + Map queryMap = new HashMap(); + queryMap.put("event", String.valueOf(event)); + queryMap.put("x", String.valueOf(x)); + queryMap.put("y", String.valueOf(y)); + queryMap.put("code", String.valueOf(code)); + queryMap.put("modifier", String.valueOf(modifiers)); + handleClientEvent(viewer, event, queryMap); + } else { + // keyboard event + event = Integer.parseInt(tokens[parsePos++]); + code = Integer.parseInt(tokens[parsePos++]); + modifiers = Integer.parseInt(tokens[parsePos++]); + + Map queryMap = new HashMap(); + queryMap.put("event", String.valueOf(event)); + queryMap.put("code", String.valueOf(code)); + queryMap.put("modifier", String.valueOf(modifiers)); + handleClientEvent(viewer, event, queryMap); + } + } + } catch(NumberFormatException e) { + s_logger.warn("Exception in handle client event bag: " + data + ", ", e); + } catch(Exception e) { + s_logger.warn("Exception in handle client event bag: " + data + ", ", e); + } + } + } + + private void handleClientEvent(ConsoleProxyViewer viewer, int event, Map queryMap) { + int code = 0; + int x = 0, y = 0; + int modifiers = 0; + + String str; + switch(event) { + case 1: // mouse move + case 2: // mouse down + case 3: // mouse up + case 8: // mouse double click + str = queryMap.get("x"); + if(str != null) { + try { + x = Integer.parseInt(str); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + str); + throw new IllegalArgumentException(e); + } + } + str = queryMap.get("y"); + if(str != null) { + try { + y = Integer.parseInt(str); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + str); + throw new IllegalArgumentException(e); + } + } + + if(event != 1) { + str = queryMap.get("code"); + try { + code = Integer.parseInt(str); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + str); + throw new IllegalArgumentException(e); + } + + str = queryMap.get("modifier"); + try { + modifiers = Integer.parseInt(str); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + str); + throw new IllegalArgumentException(e); + } + + if(s_logger.isTraceEnabled()) + s_logger.trace("Handle client mouse event. event: " + event + ", x: " + x + ", y: " + y + ", button: " + code + ", modifier: " + modifiers); + } else { + if(s_logger.isTraceEnabled()) + s_logger.trace("Handle client mouse move event. x: " + x + ", y: " + y); + } + viewer.sendClientMouseEvent(event, x, y, code, modifiers); + break; + + case 4: // key press + case 5: // key down + case 6: // key up + str = queryMap.get("code"); + try { + code = Integer.parseInt(str); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + str); + throw new IllegalArgumentException(e); + } + + str = queryMap.get("modifier"); + try { + modifiers = Integer.parseInt(str); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + str); + throw new IllegalArgumentException(e); + } + + if(s_logger.isDebugEnabled()) + s_logger.debug("Handle client keyboard event. event: " + event + ", code: " + code + ", modifier: " + modifiers); + + if(ConsoleProxy.keyboardType == ConsoleProxy.KEYBOARD_RAW) + viewer.sendClientRawKeyboardEvent(event, code, modifiers); + else + viewer.sendClientKeyboardEvent(event, code, modifiers); + break; + + default : + break; + } + } + + private void handleClientKickoff(HttpExchange t, ConsoleProxyViewer viewer) throws IOException { + String response = viewer.onAjaxClientKickoff(); + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + try { + os.write(response.getBytes()); + } finally { + os.close(); + } + } + + private void handleClientStart(HttpExchange t, ConsoleProxyViewer viewer, String title) throws IOException { + String response = viewer.onAjaxClientStart(title); + + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "text/html"); + hds.set("Cache-Control", "no-cache"); + hds.set("Cache-Control", "no-store"); + t.sendResponseHeaders(200, response.length()); + + OutputStream os = t.getResponseBody(); + try { + os.write(response.getBytes()); + } finally { + os.close(); + } + } + + private void handleClientUpdate(HttpExchange t, ConsoleProxyViewer viewer) throws IOException { + String response = viewer.onAjaxClientUpdate(); + + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "text/javascript"); + t.sendResponseHeaders(200, response.length()); + + OutputStream os = t.getResponseBody(); + try { + os.write(response.getBytes()); + } finally { + os.close(); + } + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java new file mode 100644 index 00000000000..66cba4129b4 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +import com.cloud.console.Logger; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public class ConsoleProxyAjaxImageHandler implements HttpHandler { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyAjaxImageHandler.class); + + public void handle(HttpExchange t) throws IOException { + try { + if(s_logger.isDebugEnabled()) + s_logger.debug("AjaxImageHandler " + t.getRequestURI()); + + long startTick = System.currentTimeMillis(); + + doHandle(t); + + if(s_logger.isDebugEnabled()) + s_logger.debug(t.getRequestURI() + "Process time " + (System.currentTimeMillis() - startTick) + " ms"); + } catch (IOException e) { + throw e; + } catch (IllegalArgumentException e) { + s_logger.warn("Exception, ", e); + t.sendResponseHeaders(400, -1); // bad request + } catch(Throwable e) { + s_logger.error("Unexpected exception, ", e); + t.sendResponseHeaders(500, -1); // server error + } finally { + t.close(); + } + } + + private void doHandle(HttpExchange t) throws Exception, IllegalArgumentException { + String queries = t.getRequestURI().getQuery(); + Map queryMap = getQueryMap(queries); + + String host = queryMap.get("host"); + String portStr = queryMap.get("port"); + String sid = queryMap.get("sid"); + String tag = queryMap.get("tag"); + String keyStr = queryMap.get("key"); + int key = 0; + + if(tag == null) + tag = ""; + + int port; + if(host == null || portStr == null || sid == null) + throw new IllegalArgumentException(); + + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + s_logger.warn("Invalid numeric parameter in query string: " + portStr); + throw new IllegalArgumentException(e); + } + + try { + key = Integer.parseInt(keyStr); + } catch (NumberFormatException e) { + s_logger.warn("Invalid numeric parameter in query string: " + keyStr); + throw new IllegalArgumentException(e); + } + + ConsoleProxyViewer viewer = ConsoleProxy.getVncViewer(host, port, sid, tag); + byte[] img = viewer.getAjaxImageCache().getImage(key); + if(img != null) { + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "image/jpeg"); + t.sendResponseHeaders(200, img.length); + + OutputStream os = t.getResponseBody(); + try { + os.write(img, 0, img.length); + } finally { + os.close(); + } + } else { + if(s_logger.isInfoEnabled()) + s_logger.info("Image has already been swept out, key: " + key); + t.sendResponseHeaders(404, -1); + } + } + + public static Map getQueryMap(String query) { + String[] params = query.split("&"); + Map map = new HashMap(); + for (String param : params) { + String name = param.split("=")[0]; + String value = param.split("=")[1]; + map.put(name, value); + } + return map; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxKeyMapper.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxKeyMapper.java new file mode 100644 index 00000000000..2f0b1a2a3c8 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyAjaxKeyMapper.java @@ -0,0 +1,248 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.awt.event.KeyEvent; +import java.util.HashMap; +import java.util.Map; + +public class ConsoleProxyAjaxKeyMapper { + + private Map actionKeyVkCodeMap; + private Map regularKeyVkCodeMap; + private Map js2javaCodeMap; + + private Map shiftedKeyCharMap; + + private static ConsoleProxyAjaxKeyMapper instance = new ConsoleProxyAjaxKeyMapper(); + + private ConsoleProxyAjaxKeyMapper() { + int code; + + // setup action char-code to vk-code map + actionKeyVkCodeMap = new HashMap(); + + actionKeyVkCodeMap.put(new Integer(27), new Integer(KeyEvent.VK_ESCAPE)); // Esc + actionKeyVkCodeMap.put(new Integer(9), new Integer(KeyEvent.VK_TAB)); // Tab + + actionKeyVkCodeMap.put(new Integer(112), new Integer(KeyEvent.VK_F1)); // F1 + actionKeyVkCodeMap.put(new Integer(113), new Integer(KeyEvent.VK_F2)); // F2 + actionKeyVkCodeMap.put(new Integer(114), new Integer(KeyEvent.VK_F3)); // F3 + actionKeyVkCodeMap.put(new Integer(115), new Integer(KeyEvent.VK_F4)); // F4 + actionKeyVkCodeMap.put(new Integer(116), new Integer(KeyEvent.VK_F5)); // F5 + actionKeyVkCodeMap.put(new Integer(117), new Integer(KeyEvent.VK_F6)); // F6 + actionKeyVkCodeMap.put(new Integer(118), new Integer(KeyEvent.VK_F7)); // F7 + actionKeyVkCodeMap.put(new Integer(119), new Integer(KeyEvent.VK_F8)); // F8 + actionKeyVkCodeMap.put(new Integer(120), new Integer(KeyEvent.VK_F9)); // F9 + actionKeyVkCodeMap.put(new Integer(121), new Integer(KeyEvent.VK_F10)); // F10 + actionKeyVkCodeMap.put(new Integer(122), new Integer(KeyEvent.VK_F11)); // F11 + actionKeyVkCodeMap.put(new Integer(123), new Integer(KeyEvent.VK_F12)); // F12 + + actionKeyVkCodeMap.put(new Integer(46), new Integer(KeyEvent.VK_DELETE)); // Del + actionKeyVkCodeMap.put(new Integer(13), new Integer(KeyEvent.VK_ENTER)); // Enter + actionKeyVkCodeMap.put(new Integer(36), new Integer(KeyEvent.VK_HOME)); // Home + actionKeyVkCodeMap.put(new Integer(38), new Integer(KeyEvent.VK_UP)); // Up + actionKeyVkCodeMap.put(new Integer(33), new Integer(KeyEvent.VK_PAGE_UP)); // PgUp + actionKeyVkCodeMap.put(new Integer(37), new Integer(KeyEvent.VK_LEFT)); // Left + actionKeyVkCodeMap.put(new Integer(39), new Integer(KeyEvent.VK_RIGHT)); // Right + actionKeyVkCodeMap.put(new Integer(35), new Integer(KeyEvent.VK_END)); // End + actionKeyVkCodeMap.put(new Integer(40), new Integer(KeyEvent.VK_DOWN)); // Down + actionKeyVkCodeMap.put(new Integer(34), new Integer(KeyEvent.VK_PAGE_DOWN));// PgDn + actionKeyVkCodeMap.put(new Integer(45), new Integer(KeyEvent.VK_INSERT)); // Ins + actionKeyVkCodeMap.put(new Integer(46), new Integer(KeyEvent.VK_DELETE)); // Del + + actionKeyVkCodeMap.put(new Integer(16), new Integer(KeyEvent.VK_SHIFT)); + actionKeyVkCodeMap.put(new Integer(18), new Integer(KeyEvent.VK_ALT)); + actionKeyVkCodeMap.put(new Integer(17), new Integer(KeyEvent.VK_CONTROL)); + actionKeyVkCodeMap.put(new Integer(20), new Integer(KeyEvent.VK_CAPS_LOCK)); + + actionKeyVkCodeMap.put(new Integer(KeyEvent.VK_BACK_SPACE), new Integer(KeyEvent.VK_BACK_SPACE)); + + // setup regular char-code to vk-code map + regularKeyVkCodeMap = new HashMap(); + code = KeyEvent.VK_A; + for(char c='A'; c <='Z'; c++, code++) + regularKeyVkCodeMap.put(new Integer((int)c), new Integer(code)); + + code = KeyEvent.VK_A; + for(char c='a'; c <='z'; c++, code++) + regularKeyVkCodeMap.put(new Integer((int)c), new Integer(code)); + + code = KeyEvent.VK_0; + for(char c='0'; c <='9'; c++, code++) + regularKeyVkCodeMap.put(new Integer((int)c), new Integer(code)); + + regularKeyVkCodeMap.put(new Integer('~'), new Integer(192)); + regularKeyVkCodeMap.put(new Integer('`'), new Integer(192)); + + regularKeyVkCodeMap.put(new Integer('!'), new Integer(49)); + regularKeyVkCodeMap.put(new Integer('@'), new Integer(50)); + regularKeyVkCodeMap.put(new Integer('#'), new Integer(51)); + regularKeyVkCodeMap.put(new Integer('$'), new Integer(52)); + regularKeyVkCodeMap.put(new Integer('%'), new Integer(53)); + regularKeyVkCodeMap.put(new Integer('^'), new Integer(54)); + regularKeyVkCodeMap.put(new Integer('&'), new Integer(55)); + regularKeyVkCodeMap.put(new Integer('*'), new Integer(56)); + regularKeyVkCodeMap.put(new Integer('('), new Integer(57)); + regularKeyVkCodeMap.put(new Integer(')'), new Integer(48)); + regularKeyVkCodeMap.put(new Integer('-'), new Integer(109)); + regularKeyVkCodeMap.put(new Integer('_'), new Integer(109)); + regularKeyVkCodeMap.put(new Integer('='), new Integer(107)); + regularKeyVkCodeMap.put(new Integer('+'), new Integer(107)); + + regularKeyVkCodeMap.put(new Integer('['), new Integer(219)); + regularKeyVkCodeMap.put(new Integer('{'), new Integer(219)); + regularKeyVkCodeMap.put(new Integer(']'), new Integer(221)); + regularKeyVkCodeMap.put(new Integer('}'), new Integer(221)); + regularKeyVkCodeMap.put(new Integer('\\'), new Integer(220)); + regularKeyVkCodeMap.put(new Integer('|'), new Integer(220)); + + regularKeyVkCodeMap.put(new Integer(';'), new Integer(59)); + regularKeyVkCodeMap.put(new Integer(':'), new Integer(59)); + regularKeyVkCodeMap.put(new Integer('\''), new Integer(222)); + regularKeyVkCodeMap.put(new Integer('"'), new Integer(222)); + + regularKeyVkCodeMap.put(new Integer(','), new Integer(188)); + regularKeyVkCodeMap.put(new Integer('<'), new Integer(188)); + regularKeyVkCodeMap.put(new Integer('.'), new Integer(190)); + regularKeyVkCodeMap.put(new Integer('>'), new Integer(190)); + regularKeyVkCodeMap.put(new Integer('/'), new Integer(191)); + regularKeyVkCodeMap.put(new Integer('?'), new Integer(191)); + + regularKeyVkCodeMap.put(new Integer(' '), new Integer(KeyEvent.VK_SPACE)); + + // + // Java script key code to AWT key code + // + js2javaCodeMap = new HashMap(); + js2javaCodeMap.put(new Integer(20), new Integer(new Integer(KeyEvent.VK_CAPS_LOCK))); + + js2javaCodeMap.put(new Integer(192), new Integer(new Integer('`'))); + + // for Firefox + js2javaCodeMap.put(new Integer(109), new Integer(new Integer('-'))); + js2javaCodeMap.put(new Integer(107), new Integer(KeyEvent.VK_EQUALS)); + + // for IE/Safari/Chrome + js2javaCodeMap.put(new Integer(189), new Integer(new Integer('-'))); + js2javaCodeMap.put(new Integer(187), new Integer(KeyEvent.VK_EQUALS)); + + js2javaCodeMap.put(new Integer(219), new Integer(KeyEvent.VK_OPEN_BRACKET)); + js2javaCodeMap.put(new Integer(221), new Integer(KeyEvent.VK_CLOSE_BRACKET)); + js2javaCodeMap.put(new Integer(220), new Integer(KeyEvent.VK_BACK_SLASH)); + + js2javaCodeMap.put(new Integer(186), new Integer(KeyEvent.VK_SEMICOLON)); + js2javaCodeMap.put(new Integer(222), new Integer(KeyEvent.VK_QUOTE)); + js2javaCodeMap.put(new Integer(13), new Integer(KeyEvent.VK_ENTER)); + + js2javaCodeMap.put(new Integer(190), new Integer(KeyEvent.VK_PERIOD)); + js2javaCodeMap.put(new Integer(188), new Integer(KeyEvent.VK_COMMA)); + js2javaCodeMap.put(new Integer(191), new Integer(KeyEvent.VK_SLASH)); + + js2javaCodeMap.put(new Integer(45), new Integer(KeyEvent.VK_INSERT)); + js2javaCodeMap.put(new Integer(46), new Integer(KeyEvent.VK_DELETE)); + + // numpad keys + js2javaCodeMap.put(new Integer(96), new Integer(KeyEvent.VK_0)); + js2javaCodeMap.put(new Integer(97), new Integer(KeyEvent.VK_1)); + js2javaCodeMap.put(new Integer(98), new Integer(KeyEvent.VK_2)); + js2javaCodeMap.put(new Integer(99), new Integer(KeyEvent.VK_3)); + js2javaCodeMap.put(new Integer(100), new Integer(KeyEvent.VK_4)); + js2javaCodeMap.put(new Integer(101), new Integer(KeyEvent.VK_5)); + js2javaCodeMap.put(new Integer(102), new Integer(KeyEvent.VK_6)); + js2javaCodeMap.put(new Integer(103), new Integer(KeyEvent.VK_7)); + js2javaCodeMap.put(new Integer(104), new Integer(KeyEvent.VK_8)); + js2javaCodeMap.put(new Integer(105), new Integer(KeyEvent.VK_9)); + + js2javaCodeMap.put(new Integer(110), new Integer(KeyEvent.VK_DELETE)); + js2javaCodeMap.put(new Integer(111), new Integer(KeyEvent.VK_SLASH)); + + js2javaCodeMap.put(new Integer(20), new Integer(0xffe5)); + js2javaCodeMap.put(new Integer(17), new Integer(0xffe3)); + js2javaCodeMap.put(new Integer(18), new Integer(0xffe9)); + + // for SHIFT transaction at proxy side + shiftedKeyCharMap = new HashMap(); + shiftedKeyCharMap.put(new Integer('1'), new Integer('!')); + shiftedKeyCharMap.put(new Integer('2'), new Integer('@')); + shiftedKeyCharMap.put(new Integer('3'), new Integer('#')); + shiftedKeyCharMap.put(new Integer('4'), new Integer('$')); + shiftedKeyCharMap.put(new Integer('5'), new Integer('%')); + shiftedKeyCharMap.put(new Integer('6'), new Integer('^')); + shiftedKeyCharMap.put(new Integer('7'), new Integer('&')); + shiftedKeyCharMap.put(new Integer('8'), new Integer('*')); + shiftedKeyCharMap.put(new Integer('9'), new Integer('(')); + shiftedKeyCharMap.put(new Integer('0'), new Integer(')')); + shiftedKeyCharMap.put(new Integer('-'), new Integer('_')); + shiftedKeyCharMap.put(new Integer('='), new Integer('+')); + shiftedKeyCharMap.put(new Integer('`'), new Integer('~')); + shiftedKeyCharMap.put(new Integer('['), new Integer('{')); + shiftedKeyCharMap.put(new Integer(']'), new Integer('}')); + shiftedKeyCharMap.put(new Integer('\\'), new Integer('|')); + shiftedKeyCharMap.put(new Integer(';'), new Integer(':')); + shiftedKeyCharMap.put(new Integer('\''), new Integer('"')); + shiftedKeyCharMap.put(new Integer(','), new Integer('<')); + shiftedKeyCharMap.put(new Integer('.'), new Integer('>')); + shiftedKeyCharMap.put(new Integer('/'), new Integer('?')); + } + + public char shiftedKeyCharFromKeyCode(int code, boolean shiftDown) { + + if(shiftDown) { + if(code >='A' && code <='Z') + return (char)code; + + if(code >='a' && code <= 'z') + return (char)('A' + (code - (int)'a')); + + if(shiftedKeyCharMap.containsKey(code)) + return (char)(shiftedKeyCharMap.get(code).intValue()); + } else { + if(code >='A' && code <='Z') + return (char)('a' + (code - (int)'A')); + } + + return (char)code; + } + + public static ConsoleProxyAjaxKeyMapper getInstance() { + return instance; + } + + public int getActionCharVkCode(int jsCode) { + Integer vkCode = actionKeyVkCodeMap.get(jsCode); + if(vkCode == null) + return -1; + return vkCode.intValue(); + } + + public int getRegularCharVkCode(int charCode) { + Integer vkCode = regularKeyVkCodeMap.get(charCode); + if(vkCode == null) + return -1; + return vkCode.intValue(); + } + + public int getJvmKeyCode(int jsKeyCode) { + Integer code = js2javaCodeMap.get(jsKeyCode); + if(code != null) + return code.intValue(); + return jsKeyCode; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyBaseServerFactoryImpl.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyBaseServerFactoryImpl.java new file mode 100644 index 00000000000..22e1a374de7 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyBaseServerFactoryImpl.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import javax.net.ssl.SSLServerSocket; + +import com.cloud.console.Logger; +import com.sun.net.httpserver.HttpServer; + +public class ConsoleProxyBaseServerFactoryImpl implements ConsoleProxyServerFactory { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyBaseServerFactoryImpl.class); + + public HttpServer createHttpServerInstance(int port) throws IOException { + if(s_logger.isInfoEnabled()) + s_logger.info("create HTTP server instance at port: " + port); + return HttpServer.create(new InetSocketAddress(port), 5); + } + + public SSLServerSocket createSSLServerSocket(int port) throws IOException { + if(s_logger.isInfoEnabled()) + s_logger.info("SSL server socket is not supported in ConsoleProxyBaseServerFactoryImpl"); + + return null; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientHandler.java new file mode 100644 index 00000000000..4c9be4cd02d --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyClientHandler.java @@ -0,0 +1,278 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.StringTokenizer; + +import com.cloud.console.Logger; +import com.cloud.console.RfbProto; + +public class ConsoleProxyClientHandler extends Thread { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyClientHandler.class); + + Socket clientSocket = null; + DataInputStream clientIns = null; + OutputStream clientOuts = null; + + public ConsoleProxyClientHandler(Socket client) { + clientSocket = client; + } + + synchronized void cleanup() { + try { + if (clientSocket != null) { + s_logger.info("Closing connection to " + + clientSocket.getInetAddress()); + + clientSocket.close(); + clientSocket = null; + } + } catch (IOException ioe) { + } + try { + if (clientIns != null) { + clientIns.close(); + clientIns = null; + } + } catch (IOException ioe) { + } + try { + if (clientOuts != null) { + clientOuts.close(); + clientOuts = null; + } + } catch (IOException ioe) { + } + } + + public void run() { + try { + String srcinfo = clientSocket.getInetAddress().getHostAddress() + + ":" + clientSocket.getPort(); + clientIns = new DataInputStream(new BufferedInputStream( + clientSocket.getInputStream())); +// clientOuts = new GZIPOutputStream(clientSocket.getOutputStream(), 65536); + clientOuts = clientSocket.getOutputStream(); + clientOuts.write("RFB 000.000\000".getBytes("US-ASCII")); + clientOuts.flush(); + int b1 = clientIns.read(); + int b2 = clientIns.read(); + if (b1 != 'V' || b2 != 'M') { + throw new Exception ("Bad header"); + } + byte[] proxyInfo = new byte[clientIns.read()]; + clientIns.readFully(proxyInfo); + String proxyString = new String(proxyInfo, "US-ASCII"); + StringTokenizer stk = new StringTokenizer(proxyString, ":\n"); + String host = stk.nextToken(); + int port = Integer.parseInt(stk.nextToken()); + String sid = stk.nextToken(); + ConsoleProxyViewer viewer = ConsoleProxy.getVncViewer(host, port, sid, ""); + + ConsoleProxy.waitForViewerToStart(viewer); + + handleClientSession(viewer, srcinfo); + } catch (Exception ioe) { + if(s_logger.isDebugEnabled()) + s_logger.debug(ioe.toString()); + } finally { + cleanup(); + } + } + + void writeServer (ConsoleProxyViewer viewer, byte[] b, int off, int len) { + viewer.writeServer(b, off, len); + } + + void writeClientU16 (int n) throws IOException { + byte[] b = new byte[2]; + b[0] = (byte) ((n >> 8) & 0xff); + b[1] = (byte) (n & 0xff); + clientOuts.write(b); + } + + void writeClientU32 (int n) throws IOException { + byte[] b = new byte[4]; + b[0] = (byte) ((n >> 24) & 0xff); + b[1] = (byte) ((n >> 16) & 0xff); + b[2] = (byte) ((n >> 8) & 0xff); + b[3] = (byte) (n & 0xff); + clientOuts.write(b); + } + + String RFB_VERSION_STRING = "RFB 003.008\n"; + + void handleClientSession(ConsoleProxyViewer viewer, String srcinfo) throws Exception { + s_logger.info("Start to handle client session"); + + viewer.setAjaxViewer(false); + + // Exchange version with client + clientOuts.write(RFB_VERSION_STRING.getBytes("US-ASCII")); + clientOuts.flush(); + byte[] clientVersion = new byte[12]; + clientIns.readFully(clientVersion); + if (!RFB_VERSION_STRING.equals(new String(clientVersion, "US-ASCII"))) { + throw new Exception("Bad client version"); + } + // Send security type -- no authentication needed + byte[] serverSecurity = new byte[2]; + serverSecurity[0] = 1; + serverSecurity[1] = 1; + clientOuts.write(serverSecurity); + clientOuts.flush(); + int clientSecurity = clientIns.read(); + if (clientSecurity != 1) { + throw new Exception("Unsupported client security type " + clientSecurity); + } + byte[] serverSecResp = new byte[4]; + serverSecResp[0] = serverSecResp[1] = serverSecResp[2] = serverSecResp[3] = 0; + clientOuts.write(serverSecResp); + clientOuts.flush(); + + // Receive and ignore client init + clientIns.read(); + + s_logger.info("Sending ServerInit w=" + viewer.rfb.framebufferWidth + + " h=" + viewer.rfb.framebufferHeight + + " bits=" + viewer.rfb.bitsPerPixel + + " depth=" + viewer.rfb.depth + + " name=" + viewer.rfb.desktopName); + // Send serverInit + writeClientU16(viewer.rfb.framebufferWidth); + writeClientU16(viewer.rfb.framebufferHeight); + clientOuts.write(viewer.rfb.bitsPerPixel); + clientOuts.write(viewer.rfb.depth); + clientOuts.write(viewer.rfb.bigEndian ? 1 : 0); + clientOuts.write(viewer.rfb.trueColour ? 1 : 0); + writeClientU16(viewer.rfb.redMax); + writeClientU16(viewer.rfb.greenMax); + writeClientU16(viewer.rfb.blueMax); + clientOuts.write(viewer.rfb.redShift); + clientOuts.write(viewer.rfb.greenShift); + clientOuts.write(viewer.rfb.blueShift); + byte[] pad = new byte[3]; + clientOuts.write(pad); + writeClientU32(viewer.rfb.desktopName.length()); + clientOuts.write(viewer.rfb.desktopName.getBytes("US-ASCII")); + clientOuts.flush(); + + // Lock the viewer to avoid race condition + synchronized (viewer) { + if (viewer.clientStream != null) { + s_logger.info("Disconnecting client link stream " + + viewer.clientStream.hashCode() + " from " + + viewer.clientStreamInfo); + viewer.clientStream.close(); + } + viewer.clientStream = clientOuts; + viewer.clientStreamInfo = srcinfo; + viewer.lastUsedTime = System.currentTimeMillis(); + + s_logger.info("Setting client link stream " + + viewer.clientStream.hashCode() + " from " + srcinfo); + } + + try { + while (!viewer.isDropped()) { + byte[] b = new byte[512]; + int nbytes = 0; + int msgType = clientIns.read(); + b[0] = (byte)msgType; + switch (msgType) { + case RfbProto.SetPixelFormat: + clientIns.readFully(b, 1, 19); + nbytes = 20; + + if(s_logger.isDebugEnabled()) + s_logger.debug("C->S RFB message SetPixelFormat, size=" + nbytes); + break; + + case RfbProto.SetEncodings: + + clientIns.read(); // padding + b[1] = 0; + int n = clientIns.readUnsignedShort(); + if (n > (512 - 4)/4) { + throw new Exception ("Too many client encodings"); + } + b[2] = (byte) ((n >> 8) & 0xff); + b[3] = (byte) (n & 0xff); + clientIns.readFully(b, 4, n * 4); + nbytes = n * 4 + 4; + + if(s_logger.isDebugEnabled()) + s_logger.debug("C->S RFB message SetEncodings, size=" + nbytes); + break; + + case RfbProto.FramebufferUpdateRequest: + clientIns.readFully(b, 1, 9); + nbytes = 10; + + if(s_logger.isDebugEnabled()) { + int i = b[1]; + int x = ((0xff & b[2]) << 8) + b[3]; + int y = ((0xff & b[4]) << 8) + b[5]; + int w = ((0xff & b[6]) << 8) + b[7]; + int h = ((0xff & b[8]) << 8) + b[9]; + + s_logger.debug("C->S RFB message FramebufferUpdateRequest, size=" + nbytes + " x=" + x + +" y=" + y + " w=" + w + " h=" + h); + } + break; + + case RfbProto.KeyboardEvent: + clientIns.readFully(b, 1, 7); + nbytes = 8; + + if(s_logger.isDebugEnabled()) + s_logger.debug("C->S RFB message KeyboardEvent, size=" + nbytes); + break; + case RfbProto.PointerEvent: + clientIns.readFully(b, 1, 5); + nbytes = 6; + + if(s_logger.isDebugEnabled()) + s_logger.debug("C->S RFB message PointerEvent, size=" + nbytes); + break; + + case RfbProto.VMOpsClientCustom: + clientIns.read(); // read and ignore, used to track liveliness + // of the client + if(s_logger.isDebugEnabled()) + s_logger.debug("C->S RFB message VMOpsClientCustom"); + break; + + default: + if(s_logger.isDebugEnabled()) + s_logger.debug("C->S unknown message type: " + msgType + ", size=" + nbytes); + throw new Exception("Bad client event type: " + msgType); + } + writeServer(viewer, b, 0, nbytes); + } + } finally { + viewer.lastUsedTime = System.currentTimeMillis(); + } + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyCmdHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyCmdHandler.java new file mode 100644 index 00000000000..32de7a34eab --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyCmdHandler.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import com.cloud.console.Logger; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public class ConsoleProxyCmdHandler implements HttpHandler { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyCmdHandler.class); + + public void handle(HttpExchange t) throws IOException { + try { + Thread.currentThread().setName("Cmd Thread " + + Thread.currentThread().getId() + " " + t.getRemoteAddress()); + s_logger.info("CmdHandler " + t.getRequestURI()); + doHandle(t); + } catch (Exception e) { + s_logger.error(e.toString(), e); + String response = "Not found"; + t.sendResponseHeaders(404, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } catch (Throwable e) { + s_logger.error(e.toString(), e); + } finally { + t.close(); + } + } + + public void doHandle(HttpExchange t) throws Exception { + String path = t.getRequestURI().getPath(); + int i = path.indexOf("/", 1); + String cmd = path.substring(i + 1); + s_logger.info("Get CMD request for " + cmd); + if (cmd.equals("getstatus")) { + ConsoleProxyStatus status = new ConsoleProxyStatus(); + status.setConnections(ConsoleProxy.connectionMap); + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "text/plain"); + t.sendResponseHeaders(200, 0); + OutputStreamWriter os = new OutputStreamWriter(t.getResponseBody()); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + gson.toJson(status, os); + os.close(); + } + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyJarHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyJarHandler.java new file mode 100644 index 00000000000..2581910739c --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyJarHandler.java @@ -0,0 +1,108 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Date; + +import com.cloud.console.Logger; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public class ConsoleProxyJarHandler implements HttpHandler { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyJarHandler.class); + + public void handle(HttpExchange t) throws IOException { + try { + Thread.currentThread().setName("JAR Thread " + + Thread.currentThread().getId() + " " + t.getRemoteAddress()); + s_logger.debug("JARHandler " + t.getRequestURI()); + doHandle(t); + } catch (Exception e) { + s_logger.error(e.toString(), e); + String response = "Not found"; + t.sendResponseHeaders(404, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } catch (Throwable e) { + s_logger.error(e.toString(), e); + } finally { + t.close(); + } + } + + @SuppressWarnings("deprecation") + public void doHandle(HttpExchange t) throws Exception { + String path = t.getRequestURI().getPath(); + + s_logger.info("Get JAR request for " + path); + int i = path.indexOf("/", 1); + String filepath = path.substring(i + 1); + i = path.lastIndexOf("."); + String extension = (i == -1) ? "" : path.substring(i + 1); + if (!extension.equals("jar")) { + throw new IllegalArgumentException(); + } + File f = new File (ConsoleProxy.jarDir + filepath); + long lastModified = f.lastModified(); + String ifModifiedSince = t.getRequestHeaders().getFirst("If-Modified-Since"); + if (ifModifiedSince != null) { + long d = Date.parse(ifModifiedSince); +// s_logger.info(Logger.INFO, "ifModified=" + d + " lastModified =" + lastModified); + // Give it 1 second grace period to account for errors introduced by + // date parsing and printing + if (d + 1000 >= lastModified) { + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "application/java-archive"); + t.sendResponseHeaders(304, -1); + s_logger.info("Sent 304 JAR file has not been " + + "modified since " + ifModifiedSince); +// s_logger.info("Req=" + t.getRequestHeaders().entrySet()); +// s_logger.info("Resp=" + hds.entrySet()); + return; + } + } + long length = f.length(); + FileInputStream fis = new FileInputStream(f); + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "application/java-archive"); + hds.set("Last-Modified", new Date(lastModified).toGMTString()); +// s_logger.info("Req=" + t.getRequestHeaders().entrySet()); +// s_logger.info("Resp=" + hds.entrySet()); + + t.sendResponseHeaders(200, length); + OutputStream os = t.getResponseBody(); + while (true) { + byte[] b = new byte[8192]; + int n = fis.read(b); + if (n < 0) { + break; + } + os.write(b, 0, n); + } + os.close(); + fis.close(); + s_logger.info("Sent JAR file " + path); + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyLoggerFactory.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyLoggerFactory.java new file mode 100644 index 00000000000..42796ca7496 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyLoggerFactory.java @@ -0,0 +1,88 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +public class ConsoleProxyLoggerFactory implements com.cloud.console.LoggerFactory { + public ConsoleProxyLoggerFactory() { + } + + public com.cloud.console.Logger getLogger(Class clazz) { + return new Log4jLogger(org.apache.log4j.Logger.getLogger(clazz)); + } + + public static class Log4jLogger extends com.cloud.console.Logger { + private org.apache.log4j.Logger logger; + + public Log4jLogger(org.apache.log4j.Logger logger) { + this.logger = logger; + } + + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + public void trace(Object message) { + logger.trace(message); + } + + public void trace(Object message, Throwable exception) { + logger.trace(message, exception); + } + + public void info(Object message) { + logger.info(message); + } + + public void info(Object message, Throwable exception) { + logger.info(message, exception); + } + + public void debug(Object message) { + logger.debug(message); + } + + public void debug(Object message, Throwable exception) { + logger.debug(message, exception); + } + + public void warn(Object message) { + logger.warn(message); + } + + public void warn(Object message, Throwable exception) { + logger.warn(message, exception); + } + + public void error(Object message) { + logger.error(message); + } + + public void error(Object message, Throwable exception) { + logger.error(message, exception); + } + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyResourceHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyResourceHandler.java new file mode 100644 index 00000000000..2e6f18db258 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyResourceHandler.java @@ -0,0 +1,183 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import com.cloud.console.Logger; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public class ConsoleProxyResourceHandler implements HttpHandler { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyResourceHandler.class); + + static Map s_mimeTypes; + static { + s_mimeTypes = new HashMap(); + s_mimeTypes.put("jar", "application/java-archive"); + s_mimeTypes.put("js", "text/javascript"); + s_mimeTypes.put("css", "text/css"); + s_mimeTypes.put("jpg", "image/jpeg"); + s_mimeTypes.put("html", "text/html"); + s_mimeTypes.put("htm", "text/html"); + s_mimeTypes.put("log", "text/plain"); + } + + static Map s_validResourceFolders; + static { + s_validResourceFolders = new HashMap(); + s_validResourceFolders.put("applet", ""); + s_validResourceFolders.put("logs", ""); + s_validResourceFolders.put("images", ""); + s_validResourceFolders.put("js", ""); + s_validResourceFolders.put("css", ""); + s_validResourceFolders.put("html", ""); + } + + public ConsoleProxyResourceHandler() { + } + + public void handle(HttpExchange t) throws IOException { + try { + if(s_logger.isDebugEnabled()) + s_logger.debug("Resource Handler " + t.getRequestURI()); + + long startTick = System.currentTimeMillis(); + + doHandle(t); + + if(s_logger.isDebugEnabled()) + s_logger.debug(t.getRequestURI() + " Process time " + (System.currentTimeMillis() - startTick) + " ms"); + } catch (IOException e) { + throw e; + } catch(Throwable e) { + s_logger.error("Unexpected exception, ", e); + t.sendResponseHeaders(500, -1); // server error + } finally { + t.close(); + } + } + + @SuppressWarnings("deprecation") + private void doHandle(HttpExchange t) throws Exception { + String path = t.getRequestURI().getPath(); + + if(s_logger.isInfoEnabled()) + s_logger.info("Get resource request for " + path); + + int i = path.indexOf("/", 1); + String filepath = path.substring(i + 1); + i = path.lastIndexOf("."); + String extension = (i == -1) ? "" : path.substring(i + 1); + String contentType = getContentType(extension); + + if(!validatePath(filepath)) { + if(s_logger.isInfoEnabled()) + s_logger.info("Resource access is forbidden, uri: " + path); + + t.sendResponseHeaders(403, -1); // forbidden + return; + } + + File f = new File ("./" + filepath); + if(f.exists()) { + long lastModified = f.lastModified(); + String ifModifiedSince = t.getRequestHeaders().getFirst("If-Modified-Since"); + if (ifModifiedSince != null) { + long d = Date.parse(ifModifiedSince); + if (d + 1000 >= lastModified) { + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", contentType); + t.sendResponseHeaders(304, -1); + + if(s_logger.isInfoEnabled()) + s_logger.info("Sent 304 file has not been " + + "modified since " + ifModifiedSince); + return; + } + } + + long length = f.length(); + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", contentType); + hds.set("Last-Modified", new Date(lastModified).toGMTString()); + t.sendResponseHeaders(200, length); + responseFileContent(t, f); + + if(s_logger.isInfoEnabled()) + s_logger.info("Sent file " + path + " with content type " + contentType); + } else { + if(s_logger.isInfoEnabled()) + s_logger.info("file does not exist" + path); + t.sendResponseHeaders(404, -1); + } + } + + private static String getContentType(String extension) { + String key = extension.toLowerCase(); + if(s_mimeTypes.containsKey(key)) { + return s_mimeTypes.get(key); + } + return "application/octet-stream"; + } + + private static void responseFileContent(HttpExchange t, File f) throws Exception { + OutputStream os = t.getResponseBody(); + FileInputStream fis = new FileInputStream(f); + while (true) { + byte[] b = new byte[8192]; + int n = fis.read(b); + if (n < 0) { + break; + } + os.write(b, 0, n); + } + fis.close(); + os.close(); + } + + private static boolean validatePath(String path) { + int i = path.indexOf("/"); + if(i == -1) { + if(s_logger.isInfoEnabled()) + s_logger.info("Invalid resource path: can not start at resource root"); + return false; + } + + if(path.contains("..")) { + if(s_logger.isInfoEnabled()) + s_logger.info("Invalid resource path: contains relative up-level navigation"); + + return false; + } + + return isValidResourceFolder(path.substring(0, i)); + } + + private static boolean isValidResourceFolder(String name) { + return s_validResourceFolders.containsKey(name); + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyServerFactory.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyServerFactory.java new file mode 100644 index 00000000000..29240b49b2f --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyServerFactory.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.io.IOException; + +import javax.net.ssl.SSLServerSocket; + +import com.sun.net.httpserver.HttpServer; + +public interface ConsoleProxyServerFactory { + HttpServer createHttpServerInstance(int port) throws IOException; + SSLServerSocket createSSLServerSocket(int port) throws IOException; +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyStatus.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyStatus.java new file mode 100644 index 00000000000..75a106097fb --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyStatus.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Hashtable; + +public class ConsoleProxyStatus { + ArrayList connections; + public ConsoleProxyStatus() { + } + public void setConnections(Hashtable connMap) { + ArrayList conns = new ArrayList(); + Enumeration e = connMap.keys(); + while (e.hasMoreElements()) { + synchronized (connMap) { + String key = e.nextElement(); + ConsoleProxyViewer viewer = connMap.get(key); + ConsoleProxyConnection conn = new ConsoleProxyConnection(); + conn.id = viewer.id; + conn.clientInfo = viewer.clientStreamInfo; + conn.host = viewer.host; + conn.port = viewer.port; + conn.tag = viewer.getTag(); + conn.createTime = viewer.createTime; + conn.lastUsedTime = viewer.lastUsedTime; + conns.add(conn); + } + } + connections = conns; + } + public static class ConsoleProxyConnection { + public int id; + public String clientInfo; + public String host; + public int port; + public String tag; + public long createTime; + public long lastUsedTime; + + public ConsoleProxyConnection() { + } + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyThumbnailHandler.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyThumbnailHandler.java new file mode 100644 index 00000000000..140ede10581 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyThumbnailHandler.java @@ -0,0 +1,266 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +import com.cloud.console.Logger; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public class ConsoleProxyThumbnailHandler implements HttpHandler { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyThumbnailHandler.class); + + public ConsoleProxyThumbnailHandler() { + } + + public void handle(HttpExchange t) throws IOException { + try { + Thread.currentThread().setName("JPG Thread " + + Thread.currentThread().getId() + " " + t.getRemoteAddress()); + + if(s_logger.isDebugEnabled()) + s_logger.debug("ScreenHandler " + t.getRequestURI()); + + long startTick = System.currentTimeMillis(); + doHandle(t); + + if(s_logger.isDebugEnabled()) + s_logger.debug(t.getRequestURI() + "Process time " + (System.currentTimeMillis() - startTick) + " ms"); + } catch (IllegalArgumentException e) { + String response = "Bad query string"; + s_logger.error(response + ", request URI : " + t.getRequestURI()); + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } catch (Throwable e) { + s_logger.error("Unexpected exception while handing thumbnail request, ", e); + + String queries = t.getRequestURI().getQuery(); + Map queryMap = getQueryMap(queries); + int width = 0; + int height = 0; + String ws = queryMap.get("w"); + String hs = queryMap.get("h"); + try { + width = Integer.parseInt(ws); + height = Integer.parseInt(hs); + } catch (NumberFormatException ex) { + } + width = Math.min(width, 800); + height = Math.min(height, 600); + + BufferedImage img = generateTextImage(width, height, "Cannot Connect"); + ByteArrayOutputStream bos = new ByteArrayOutputStream(8196); + javax.imageio.ImageIO.write(img, "jpg", bos); + byte[] bs = bos.toByteArray(); + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "image/jpeg"); + hds.set("Cache-Control", "no-cache"); + hds.set("Cache-Control", "no-store"); + t.sendResponseHeaders(200, bs.length); + OutputStream os = t.getResponseBody(); + os.write(bs); + os.close(); + +/* + // Send back a dummy image + File f = new File ("./images/cannotconnect.jpg"); + long length = f.length(); + FileInputStream fis = new FileInputStream(f); + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "image/jpeg"); + hds.set("Cache-Control", "no-cache"); + hds.set("Cache-Control", "no-store"); + t.sendResponseHeaders(200, length); + OutputStream os = t.getResponseBody(); + try { + while (true) { + byte[] b = new byte[8192]; + int n = fis.read(b); + if (n < 0) { + break; + } + os.write(b, 0, n); + } + } finally { + os.close(); + fis.close(); + } +*/ + s_logger.error("Cannot get console, sent error JPG response for " + t.getRequestURI()); + return; + } finally { + t.close(); + } + } + + private void doHandle(HttpExchange t) throws Exception, + IllegalArgumentException { + String queries = t.getRequestURI().getQuery(); + Map queryMap = getQueryMap(queries); + int width = 0; + int height = 0; + int port = 0; + String ws = queryMap.get("w"); + String hs = queryMap.get("h"); + String host = queryMap.get("host"); + String portStr = queryMap.get("port"); + String sid = queryMap.get("sid"); + String tag = queryMap.get("tag"); + if(tag == null) + tag = ""; + + if (ws == null || hs == null || host == null || portStr == null || sid == null ) { + throw new IllegalArgumentException(); + } + try { + width = Integer.parseInt(ws); + height = Integer.parseInt(hs); + port = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(e); + } + + ConsoleProxyViewer viewer = ConsoleProxy.getVncViewer(host, port, sid, tag); + + if (viewer.status != ConsoleProxyViewer.STATUS_NORMAL_OPERATION) { + // use generated image instead of static + BufferedImage img = generateTextImage(width, height, "Connecting"); + ByteArrayOutputStream bos = new ByteArrayOutputStream(8196); + javax.imageio.ImageIO.write(img, "jpg", bos); + byte[] bs = bos.toByteArray(); + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "image/jpeg"); + hds.set("Cache-Control", "no-cache"); + hds.set("Cache-Control", "no-store"); + t.sendResponseHeaders(200, bs.length); + OutputStream os = t.getResponseBody(); + os.write(bs); + os.close(); + +/* + // Send back a dummy image + File f = new File ("./images/notready.jpg"); + long length = f.length(); + FileInputStream fis = new FileInputStream(f); + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "image/jpeg"); + hds.set("Cache-Control", "no-cache"); + hds.set("Cache-Control", "no-store"); + t.sendResponseHeaders(200, length); + + OutputStream os = t.getResponseBody(); + try { + while (true) { + byte[] b = new byte[8192]; + int n = fis.read(b); + if (n < 0) { + break; + } + os.write(b, 0, n); + } + } finally { + os.close(); + fis.close(); + } +*/ + if(s_logger.isInfoEnabled()) + s_logger.info("Console not ready, sent dummy JPG response, viewer status : " + viewer.status); + return; + } +/* + if (viewer.status == ConsoleViewer.STATUS_AUTHENTICATION_FAILURE) { + String response = "Authentication failed"; + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } else if (viewer.vc == null || viewer.vc.memImage == null) { + String response = "Server not ready"; + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } else +*/ + { + Image scaledImage = viewer.vc.memImage.getScaledInstance(width, + height, Image.SCALE_DEFAULT); + BufferedImage bufferedImage = new BufferedImage(width, height, + BufferedImage.TYPE_3BYTE_BGR); + Graphics2D bufImageGraphics = bufferedImage.createGraphics(); + bufImageGraphics.drawImage(scaledImage, 0, 0, null); + ByteArrayOutputStream bos = new ByteArrayOutputStream(8196); + javax.imageio.ImageIO.write(bufferedImage, "jpg", bos); + byte[] bs = bos.toByteArray(); + Headers hds = t.getResponseHeaders(); + hds.set("Content-Type", "image/jpeg"); + hds.set("Cache-Control", "no-cache"); + hds.set("Cache-Control", "no-store"); + t.sendResponseHeaders(200, bs.length); + OutputStream os = t.getResponseBody(); + os.write(bs); + os.close(); + } + } + + public static BufferedImage generateTextImage(int w, int h, String text) { + BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_3BYTE_BGR); + Graphics2D g = img.createGraphics(); + g.setColor(Color.BLACK); + g.fillRect(0, 0, w, h); + g.setColor(Color.WHITE); + try { + g.setFont(new Font(null, Font.PLAIN, 12)); + FontMetrics fm = g.getFontMetrics(); + int textWidth = fm.stringWidth(text); + int startx = (w-textWidth) / 2; + if(startx < 0) + startx = 0; + g.drawString(text, startx, h/2); + } catch (Throwable e) { + s_logger.warn("Problem in generating text to thumnail image, return blank image"); + } + return img; + } + + public static Map getQueryMap(String query) { + String[] params = query.split("&"); + Map map = new HashMap(); + for (String param : params) { + String name = param.split("=")[0]; + String value = param.split("=")[1]; + map.put(name, value); + } + return map; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyViewer.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyViewer.java new file mode 100644 index 00000000000..754d8f6727e --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyViewer.java @@ -0,0 +1,1384 @@ +/** + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + + +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPOutputStream; + +import org.apache.log4j.Logger; + +import com.cloud.console.AuthenticationException; +import com.cloud.console.ConsoleCanvas; +import com.cloud.console.ConsoleCanvas2; +import com.cloud.console.ITileScanListener; +import com.cloud.console.Region; +import com.cloud.console.RfbProto; +import com.cloud.console.RfbProtoAdapter; +import com.cloud.console.RfbViewer; +import com.cloud.console.TileInfo; +import com.cloud.console.TileTracker; + +public class ConsoleProxyViewer implements java.lang.Runnable, RfbViewer, RfbProtoAdapter, ITileScanListener { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyViewer.class); + + public final static int STATUS_ERROR = -1; + public final static int STATUS_UNINITIALIZED = 0; + public final static int STATUS_CONNECTING = 1; + public final static int STATUS_INITIALIZING = 2; + public final static int STATUS_NORMAL_OPERATION = 3; + public final static int STATUS_AUTHENTICATION_FAILURE = 100; + + public final static int SHIFT_KEY_MASK = 64; + public final static int CTRL_KEY_MASK = 128; + public final static int META_KEY_MASK = 256; + public final static int ALT_KEY_MASK = 512; + + int id = getNextId(); + boolean compressServerMessage = false; + long createTime = System.currentTimeMillis(); + long lastUsedTime = System.currentTimeMillis(); + int status; + boolean dropMe = false; + boolean viewerInReuse = false; + + String host; + int port; + String tag = ""; + + RfbProto rfb; + Thread rfbThread; + OutputStream clientStream; + String clientStreamInfo; + String passwordParam; + + ViewerOptions options; + Frame vncFrame; + ConsoleCanvas vc; + Container vncContainer; + + boolean ajaxViewer = false; + long ajaxSessionId = 0; + TileTracker tracker; + Object tileDirtyEvent; + boolean dirtyFlag = false; + boolean justCreated = true; + AjaxFIFOImageCache ajaxImageCache = new AjaxFIFOImageCache(2); + + String cursorUpdatesDef; + String eightBitColorsDef; + + int deferScreenUpdates; + int deferCursorUpdates; + int deferUpdateRequests; + + int[] encodingsSaved; + int nEncodingsSaved; + + boolean framebufferResized = false; + int resizedFramebufferWidth; + int resizedFramebufferHeight; + + boolean cursorMoved = false; + int lastCursorPosX; + int lastCursorPosY; + + boolean cursorShapeChanged = false; + int lastCursorShapeEncodingType; + int lastCursorShapeHotX; + int lastCursorShapeHotY; + int lastCursorShapeWidth; + int lastCursorShapeHeight; + byte[] lastCursorShapeData; + + static int id_count = 1; + synchronized static int getNextId() { + return id_count++; + } + + public void init() { + initProxy(); + } + + private void initProxy() { + options = new ViewerOptions(); + options.viewOnly = true; + + cursorUpdatesDef = null; + eightBitColorsDef = null; + + tracker = new TileTracker(); + tracker.initTracking(64, 64, 800, 600); + + if(rfbThread != null) { + if(rfbThread.isAlive()) { + dropMe = true; + viewerInReuse = true; + if(rfb != null) + rfb.close(); + + try { + rfbThread.join(); + } catch (InterruptedException e) { + s_logger.warn("InterruptedException while waiting for RFB thread to exit"); + } + viewerInReuse = false; + } + } + + dropMe = false; + rfbThread = new Thread(this); + rfbThread.setName("RFB Thread " + rfbThread.getId() + " >" + host + ":" + port); + rfbThread.start(); + + tileDirtyEvent = new Object(); + } + + public synchronized boolean justCreated() { + if(justCreated) { + justCreated = false; + return true; + } + return false; + } + + public boolean isDropped() { + return dropMe; + } + + public void run() { + createCanvas(0, 0); + + int retries = 0; + while (!dropMe) { + try { + s_logger.info("Connecting to VNC server"); + status = STATUS_CONNECTING; + connectAndAuthenticate(); + retries = 0; // reset the retry count + status = STATUS_INITIALIZING; + doProtocolInitialisation(); + vc.rfb = rfb; + vc.setPixelFormat(); + + // if we have a client current connected, when we have reconnected to the server and + // received a new ServerInit info (in doProtocolInitialisation()), we will + // convert it into frame buffer size change to make sure following on updates + // don't fall out of range + // + if(clientStream != null) { + // 128 bytes will be enough for this single PDU + s_logger.info("Send init framebuffer size (" + rfb.framebufferWidth + ", " + rfb.framebufferHeight + ")"); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(128); + try { + vc.encodeFramebufferResize(rfb.framebufferWidth, rfb.framebufferHeight, bos); + } catch(IOException e) { + } + writeToClientStream(bos.toByteArray()); + } + + vc.rfb.writeFramebufferUpdateRequest(0, 0, + vc.rfb.framebufferWidth, vc.rfb.framebufferHeight, + true); + status = STATUS_NORMAL_OPERATION; + vc.processNormalProtocol(); + } catch (AuthenticationException e) { + status = STATUS_AUTHENTICATION_FAILURE; + String msg = e.getMessage(); + s_logger.warn("Authentication exception, msg: " + msg + "sid: " + this.passwordParam); + } catch (Exception e) { + status = STATUS_ERROR; + if(s_logger.isDebugEnabled()) + s_logger.debug("Exception : ", e); + } finally { + // String oldName = Thread.currentThread().getName(); + encodingsSaved = null; + nEncodingsSaved = 0; + + s_logger.info("Close current RFB"); + synchronized (this) { + if (rfb != null) { + rfb.close(); + } + } + } + if (dropMe) { + break; + } + if (status == STATUS_AUTHENTICATION_FAILURE) { + break; + } else { + retries++; + if(retries > ConsoleProxy.reconnectMaxRetry) { + s_logger.info("Exception caught, retry has reached to maximum : " + retries + ", will give up and disconnect client"); + break; + } + + s_logger.info("Exception caught, retrying in 1 second, current retry:" + retries); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // ignored + } + } + } + + // make sure we remove it from the management map upon main thread termination + dropMe = true; + + // if we are reusing the viewer object, we shouldn't remove it from the map + // this can also prevent deadlock in initProxy() while initProxy tries to join + // the thread, as initProxy() is called with ConsoleProxy.connectionMap being locked + // while CoonsoleProxy.removeViewer() here will attempt to lock it from another thread + if(!viewerInReuse) + ConsoleProxy.removeViewer(this); + s_logger.info("RFB thread terminating"); + } + + void connectAndAuthenticate() throws Exception { + s_logger.info("Initializing..."); + + s_logger.info("Ensure ip route towards host " + host); + ConsoleProxy.ensureRoute(host); + + s_logger.info("Connecting to " + host + ", port " + port + "..."); + rfb = new RfbProto(host, port, this); + s_logger.info("Connected to server"); + + rfb.readVersionMsg(); + s_logger.info("RFB server supports protocol version " + + rfb.serverMajor + "." + rfb.serverMinor); + + rfb.writeVersionMsg(); + s_logger.info("Using RFB protocol version " + rfb.clientMajor + + "." + rfb.clientMinor); + + int secType = rfb.negotiateSecurity(); + int authType; + if (secType == RfbProto.SecTypeTight) { + s_logger.info("Enabling TightVNC protocol extensions"); + rfb.initCapabilities(); + rfb.setupTunneling(); + authType = rfb.negotiateAuthenticationTight(); + } else { + authType = secType; + } + + switch (authType) { + case RfbProto.AuthNone: + s_logger.info("No authentication needed"); + rfb.authenticateNone(); + break; + case RfbProto.AuthVNC: + s_logger.info("Performing standard VNC authentication"); + if (passwordParam != null) { + rfb.authenticateVNC(passwordParam); + } else { + throw new AuthenticationException("Bad password"); + } + break; + default: + throw new Exception("Unknown authentication scheme " + authType); + } + } + + static void authenticationExternally(String tag, String sid) throws AuthenticationException { +/* + if(ConsoleProxy.management_host != null) { + try { + boolean success = false; + URL url = new URL(ConsoleProxy.management_host + "/console?cmd=auth&vm=" + getTag() + "&sid=" + passwordParam); + + URLConnection conn = url.openConnection(); + + // setting TIMEOUTs to avoid possible waiting until death situations + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String inputLine; + if ((inputLine = in.readLine()) != null) { + if(inputLine.equals("success")) + success = true; + } + in.close(); + + if(!success) { + if(s_logger.isInfoEnabled()) + s_logger.info("External authenticator failed authencation request for vm " + getTag() + " with sid " + passwordParam); + + throw new AuthenticationException("Unable to contact external authentication source " + ConsoleProxy.management_host); + } + } catch (MalformedURLException e) { + s_logger.error("Unexpected exception " + e.getMessage(), e); + } catch(IOException e) { + s_logger.error("Unable to contact external authentication source due to " + e.getMessage(), e); + throw new AuthenticationException("Unable to contact external authentication source " + ConsoleProxy.management_host); + } + } else { + s_logger.warn("No external authentication source being setup."); + } +*/ + if(!ConsoleProxy.authenticateConsoleAccess(tag, sid)) { + s_logger.warn("External authenticator failed authencation request for vm " + tag + " with sid " + sid); + + throw new AuthenticationException("External authenticator failed request for vm " + tag + " with sid " + sid); + } + } + + void doProtocolInitialisation() throws IOException { + rfb.writeClientInit(); + rfb.readServerInit(); + setEncodings(); + } + + void setEncodings() { + setEncodings(false); + } + + void setEncodings(boolean autoSelectOnly) { + if (options == null || rfb == null || !rfb.inNormalProtocol) + return; + + int preferredEncoding = options.preferredEncoding; + if (preferredEncoding == -1) { + long kbitsPerSecond = rfb.kbitsPerSecond(); + if (nEncodingsSaved < 1) { + // Choose Tight or ZRLE encoding for the very first update. + // Logger.log(Logger.INFO, "Using Tight/ZRLE encodings"); + preferredEncoding = RfbProto.EncodingTight; + } else if (kbitsPerSecond > 2000 + && encodingsSaved[0] != RfbProto.EncodingHextile) { + // Switch to Hextile if the connection speed is above 2Mbps. + s_logger.info("Throughput " + kbitsPerSecond + + " kbit/s - changing to Hextile encoding"); + preferredEncoding = RfbProto.EncodingHextile; + } else if (kbitsPerSecond < 1000 + && encodingsSaved[0] != RfbProto.EncodingTight) { + // Switch to Tight/ZRLE if the connection speed is below 1Mbps. + s_logger.info("Throughput " + kbitsPerSecond + + " kbit/s - changing to Tight/ZRLE encodings"); + preferredEncoding = RfbProto.EncodingTight; + } else { + // Don't change the encoder. + if (autoSelectOnly) + return; + preferredEncoding = encodingsSaved[0]; + } + } else { + // Auto encoder selection is not enabled. + if (autoSelectOnly) + return; + } + + int[] encodings = new int[20]; + int nEncodings = 0; + + encodings[nEncodings++] = preferredEncoding; + if (options.useCopyRect) { + encodings[nEncodings++] = RfbProto.EncodingCopyRect; + } + + if (preferredEncoding != RfbProto.EncodingTight) { + encodings[nEncodings++] = RfbProto.EncodingTight; + } + if (preferredEncoding != RfbProto.EncodingZRLE) { + encodings[nEncodings++] = RfbProto.EncodingZRLE; + } + if (preferredEncoding != RfbProto.EncodingHextile) { + encodings[nEncodings++] = RfbProto.EncodingHextile; + } + if (preferredEncoding != RfbProto.EncodingZlib) { + encodings[nEncodings++] = RfbProto.EncodingZlib; + } + if (preferredEncoding != RfbProto.EncodingCoRRE) { + encodings[nEncodings++] = RfbProto.EncodingCoRRE; + } + if (preferredEncoding != RfbProto.EncodingRRE) { + encodings[nEncodings++] = RfbProto.EncodingRRE; + } + + if (options.compressLevel >= 0 && options.compressLevel <= 9) { + encodings[nEncodings++] = RfbProto.EncodingCompressLevel0 + + options.compressLevel; + } + if (options.jpegQuality >= 0 && options.jpegQuality <= 9) { + encodings[nEncodings++] = RfbProto.EncodingQualityLevel0 + + options.jpegQuality; + } + + if (options.requestCursorUpdates) { + encodings[nEncodings++] = RfbProto.EncodingXCursor; + encodings[nEncodings++] = RfbProto.EncodingRichCursor; + if (!options.ignoreCursorUpdates) + encodings[nEncodings++] = RfbProto.EncodingPointerPos; + } + + encodings[nEncodings++] = RfbProto.EncodingLastRect; + encodings[nEncodings++] = RfbProto.EncodingNewFBSize; + + boolean encodingsWereChanged = false; + if (nEncodings != nEncodingsSaved) { + encodingsWereChanged = true; + } else { + for (int i = 0; i < nEncodings; i++) { + if (encodings[i] != encodingsSaved[i]) { + encodingsWereChanged = true; + break; + } + } + } + + if (encodingsWereChanged) { + try { + rfb.writeSetEncodings(encodings, nEncodings); + if (vc != null) { + vc.softCursorFree(); + } + } catch (Exception e) { + s_logger.error(e.toString(), e); + } + encodingsSaved = encodings; + nEncodingsSaved = nEncodings; + } + } + + protected void startRecording() throws IOException { + } + + protected void stopRecording() throws IOException { + } + + void createCanvas(int maxWidth, int maxHeight) { + vc = new ConsoleCanvas2(this, maxWidth, maxHeight); + + if (!options.viewOnly) + vc.enableInput(true); + } + + synchronized void writeToClientStream(byte[] bs) { + // writeToClientStream swallows exceptions to make sure problems writing + // to client stream do not impact the main loop + if (clientStream != null) { + try { + lastUsedTime = System.currentTimeMillis(); + synchronized (clientStream) { + clientStream.write(bs); + clientStream.flush(); + } + } catch (IOException e) { + if(s_logger.isDebugEnabled()) { + s_logger.debug("Writing to client stream failed, reason: " + e.getMessage()); + } + try { + clientStream.close(); + } catch (IOException ioe){ + // ignore + } + clientStream = null; + clientStreamInfo = null; + } + } + } + + // + // Implement RfbViewer interface + // + public boolean isProxy() { + return true; + } + + public boolean hasClientConnection() { + // always return false if the viewer is AJAX viewer + if(ajaxViewer) + return false; + + return clientStream != null; + } + + public RfbProto getRfb() { + return rfb; + } + + public Dimension getScreenSize() { + return (new Frame()).getToolkit().getScreenSize(); + // return vncFrame.getToolkit().getScreenSize(); + } + + public Dimension getFrameSize() { + // return vncFrame.getSize(); + return getScreenSize(); + } + + public int getScalingFactor() { + return options.scalingFactor; + } + + public int getCursorScaleFactor() { + return options.scaleCursor; + } + + public boolean ignoreCursorUpdate() { + return options.ignoreCursorUpdates; + } + + public int getDeferCursorUpdateTimeout() { + return 0; + // return deferCursorUpdates; + } + + public int getDeferScreenUpdateTimeout() { + return 0; + // return deferScreenUpdates; + } + + public int getDeferUpdateRequestTimeout() { + return 0; + //return deferUpdateRequests; + } + + public int setPixelFormat(RfbProto rfb) throws IOException { + if (options.eightBitColors) { + rfb.writeSetPixelFormat(8, 8, false, true, 7, 7, 3, 0, 3, 6); + return 1; + } else { + rfb.writeSetPixelFormat(32, 24, false, true, 255, 255, 255, 16, 8, 0); + return 4; + } + } + + public void onInputEnabled(boolean enable) { + // do nothing in proxy viewer + } + + public void onFramebufferSizeChange(int w, int h) { + tracker.resize(vc.scaledWidth, vc.scaledHeight); + + synchronized(this) { + framebufferResized = true; + resizedFramebufferWidth = w; + resizedFramebufferHeight = h; + } + + signalTileDirtyEvent(); + } + + public void onFramebufferUpdate(int x, int y, int w, int h) { + if(s_logger.isTraceEnabled()) + s_logger.trace("Frame buffer update {" + x + "," + y + "," + w + "," + h + "}"); + tracker.invalidate(new Rectangle(x, y, w, h)); + + signalTileDirtyEvent(); + } + + public void onFramebufferCursorMove(int x, int y) { + synchronized(this) { + cursorMoved = true; + lastCursorPosX = x; + lastCursorPosY = y; + } + + signalTileDirtyEvent(); + } + + public void onFramebufferCursorShapeChange(int encodingType, + int xhot, int yhot, int width, int height, byte[] cursorData) { + + synchronized(this) { + cursorShapeChanged = true; + + lastCursorShapeEncodingType = encodingType; + lastCursorShapeHotX = xhot; + lastCursorShapeHotY = yhot; + lastCursorShapeWidth = width; + lastCursorShapeHeight = height; + lastCursorShapeData = cursorData; + } + + signalTileDirtyEvent(); + } + + public void onDesktopResize() { + if(vncFrame != null) + vncFrame.pack(); + } + + public void onFrameResize(Dimension newSize) { + if(vncFrame != null) + vncFrame.setSize(newSize); + } + + public void onDisconnectMessage() { + // do nothing in viewer mode + } + + public void onBellMessage() { + Toolkit.getDefaultToolkit().beep(); + } + + public void onPreProtocolProcess(byte[] bs) throws IOException { + + if(s_logger.isTraceEnabled()) + s_logger.trace("Send " + (bs != null ? bs.length : 0) + " bytes (original) to client"); + + if (!ajaxViewer && bs != null && clientStream != null) { + if(s_logger.isInfoEnabled()) + s_logger.info("getSplit got " + bs.length + " bytes"); + if (compressServerMessage && bs.length > 10000) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(256000); + GZIPOutputStream gos = new GZIPOutputStream(bos, 65536); + gos.write(bs); + gos.finish(); + byte[] nbs = bos.toByteArray(); + gos.close(); + int n = nbs.length; + + if(s_logger.isInfoEnabled()) + s_logger.info("Compressed " + bs.length + "=>" + n); + + byte[] b = new byte[6]; + b[0] = (byte) 250; + b[1] = 2; + b[2] = (byte) ((n >> 24) & 0xff); + b[3] = (byte) ((n >> 16) & 0xff); + b[4] = (byte) ((n >> 8) & 0xff); + b[5] = (byte) (n & 0xff); + + // make sure two seperated writes completed atomically + synchronized(clientStream) { + writeToClientStream(b); + writeToClientStream(nbs); + } + } else { + if(s_logger.isInfoEnabled()) + s_logger.info("Send uncompressed " + bs.length + " bytes to client"); + + writeToClientStream(bs); + } + } else { + if(s_logger.isTraceEnabled()) + s_logger.trace("Client is not connected, ignore forwarding " + (bs != null ? bs.length : 0) + " bytes to client"); + } + + rfb.sis.setSplit(); + } + + public boolean onPostFrameBufferUpdateProcess(boolean cursorPosReceived) throws IOException { + boolean fullUpdateNeeded = false; + + // Defer framebuffer update request if necessary. But wake up + // immediately on keyboard or mouse event. Also, don't sleep + // if there is some data to receive, or if the last update + // included a PointerPos message. + if (deferUpdateRequests > 0 && rfb.is.available() == 0 && !cursorPosReceived) { + synchronized(vc.rfb) { + try { + vc.rfb.wait(deferUpdateRequests); + } catch (InterruptedException e) { + } + } + } + + // Before requesting framebuffer update, check if the pixel + // format should be changed. If it should, request full update + // instead of an incremental one. + if (options.eightBitColors != (vc.bytesPixel == 1)) { + vc.setPixelFormat(); + fullUpdateNeeded = true; + } + + return fullUpdateNeeded; + } + + public void onProtocolProcessException(IOException e) { + byte[] bs = new byte[2]; + bs[0] = (byte)250; + bs[1] = 1; + writeToClientStream(bs); + } + + public Socket createConnection(String host, int port) throws IOException { + Socket sock = new Socket(); + sock.setSoTimeout(ConsoleProxy.readTimeoutSeconds*1000); + sock.setKeepAlive(true); + sock.connect(new InetSocketAddress(host, port), 30000); + return sock; + } + + public void writeInit(OutputStream os) throws IOException { + if (options.shareDesktop) { + os.write(1); + } else { + os.write(0); + } + } + + public void swapMouseButton(Integer[] masks) { + if (options.reverseMouseButtons2And3) { + Integer temp = masks[1]; + masks[1] = masks[0]; + masks[0] = temp; + } + } + + public boolean onTileChange(Rectangle rowMergedRect, int row, int col) { + // currently we don't do scan-based client update + return true; + } + + public void onRegionChange(List regionList) { + // obsolute + } + + private void signalTileDirtyEvent() { + synchronized(tileDirtyEvent) { + dirtyFlag = true; + tileDirtyEvent.notifyAll(); + } + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + // + // AJAX Image manipulation + // + public void copyTile(Graphics2D g, int x, int y, Rectangle rc) { + if(vc != null && vc.memImage != null) { + synchronized(vc.memImage) { + g.drawImage(vc.memImage, x, y, x + rc.width, y + rc.height, + rc.x, rc.y, rc.x + rc.width, rc.y + rc.height, null); + } + } + } + + public byte[] getFrameBufferJpeg() { + int width = 800; + int height = 600; + if(vc != null) { + width = vc.scaledWidth; + height = vc.scaledHeight; + } + + if(s_logger.isTraceEnabled()) + s_logger.trace("getFrameBufferJpeg, w: " + width + ", h: " + height); + + BufferedImage bufferedImage = new BufferedImage(width, height, + BufferedImage.TYPE_3BYTE_BGR); + if(vc != null && vc.memImage != null) { + synchronized(vc.memImage) { + Graphics2D g = bufferedImage.createGraphics(); + g.drawImage(vc.memImage, 0, 0, width, height, 0, 0, width, height, null); + } + } + + byte[] imgBits = null; + try { + imgBits = jpegFromImage(bufferedImage); + } catch (IOException e) { + } + return imgBits; + } + + public byte[] getTilesMergedJpeg(List tileList, int tileWidth, int tileHeight) { + + int width = Math.max(tileWidth, tileWidth*tileList.size()); + BufferedImage bufferedImage = new BufferedImage(width, tileHeight, + BufferedImage.TYPE_3BYTE_BGR); + + if(s_logger.isTraceEnabled()) + s_logger.trace("Create merged image, w: " + width + ", h: " + tileHeight); + + if(vc != null && vc.memImage != null) { + synchronized(vc.memImage) { + Graphics2D g = bufferedImage.createGraphics(); + int i = 0; + for(TileInfo tile : tileList) { + Rectangle rc = tile.getTileRect(); + + if(s_logger.isTraceEnabled()) + s_logger.trace("Merge tile into jpeg from (" + rc.x + "," + rc.y + "," + (rc.x + rc.width) + "," + (rc.y + rc.height) + ") to (" + i*tileWidth + ",0)" ); + + g.drawImage(vc.memImage, i*tileWidth, 0, i*tileWidth + rc.width, rc.height, + rc.x, rc.y, rc.x + rc.width, rc.y + rc.height, null); + + i++; + } + } + } + + byte[] imgBits = null; + try { + imgBits = jpegFromImage(bufferedImage); + + if(s_logger.isTraceEnabled()) + s_logger.trace("Merge jpeg image size: " + imgBits.length + ", tiles: " + tileList.size()); + } catch (IOException e) { + } + return imgBits; + } + + public byte[] jpegFromImage(BufferedImage image) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(128000); + javax.imageio.ImageIO.write(image, "jpg", bos); + + byte[] jpegBits = bos.toByteArray(); + bos.close(); + return jpegBits; + } + + private String prepareAjaxImage(List tiles, boolean init) { + byte[] imgBits; + if(init) + imgBits = getFrameBufferJpeg(); + else + imgBits = getTilesMergedJpeg(tiles, tracker.getTileWidth(), tracker.getTileHeight()); + + if(imgBits == null) { + s_logger.warn("Unable to generate jpeg image"); + } else { + if(s_logger.isTraceEnabled()) + s_logger.trace("Generated jpeg image size: " + imgBits.length); + } + + int key = ajaxImageCache.putImage(imgBits); + StringBuffer sb = new StringBuffer("/ajaximg?host="); + sb.append(host).append("&port=").append(port).append("&sid=").append(passwordParam); + sb.append("&key=").append(key).append("&ts=").append(System.currentTimeMillis()); + return sb.toString(); + } + + private String prepareAjaxSession(boolean init) { + StringBuffer sb = new StringBuffer(); + + if(init) + ajaxSessionId++; + + sb.append("/ajax?host=").append(host).append("&port=").append(port); + sb.append("&sid=").append(passwordParam).append("&sess=").append(ajaxSessionId); + return sb.toString(); + } + + public String onAjaxClientKickoff() { + return "onKickoff();"; + } + + private boolean waitForViewerReady() { + long startTick = System.currentTimeMillis(); + while(System.currentTimeMillis() - startTick < 5000) { + if(this.status == ConsoleProxyViewer.STATUS_NORMAL_OPERATION) + return true; + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + return false; + } + + private String onAjaxClientConnectFailed() { + return "

" + + "Unable to start console session as connection is refused by the machine you are accessing" + + "

"; + } + + public String onAjaxClientStart(String title) { + if(!waitForViewerReady()) + return onAjaxClientConnectFailed(); + + // make sure we switch to AJAX view on start + setAjaxViewer(true); + + int tileWidth = tracker.getTileWidth(); + int tileHeight = tracker.getTileHeight(); + int width = tracker.getTrackWidth(); + int height = tracker.getTrackHeight(); + + if(s_logger.isTraceEnabled()) + s_logger.trace("Ajax client start, frame buffer w: " + width + ", " + height); + + synchronized(this) { + if(framebufferResized) { + framebufferResized = false; + } + } + + int retry = 0; + if(justCreated()) { + tracker.initCoverageTest(); + + try { + rfb.writeFramebufferUpdateRequest(0, 0, tracker.getTrackWidth(), tracker.getTrackHeight(), false); + + while(!tracker.hasFullCoverage() && retry < 10) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + retry++; + } + } catch (IOException e1) { + s_logger.warn("Connection was broken "); + } + } + + List tiles = tracker.scan(true); + String imgUrl = prepareAjaxImage(tiles, true); + String updateUrl = prepareAjaxSession(true); + + StringBuffer sbTileSequence = new StringBuffer(); + int i = 0; + for(TileInfo tile : tiles) { + sbTileSequence.append("[").append(tile.getRow()).append(",").append(tile.getCol()).append("]"); + if(i < tiles.size() - 1) + sbTileSequence.append(","); + + i++; + } + +/* + SimpleHash model = new SimpleHash(); + model.put("tileSequence", sbTileSequence.toString()); + model.put("imgUrl", imgUrl); + model.put("updateUrl", updateUrl); + model.put("width", String.valueOf(width)); + model.put("height", String.valueOf(height)); + model.put("tileWidth", String.valueOf(tileWidth)); + model.put("tileHeight", String.valueOf(tileHeight)); + model.put("title", title); + model.put("rawKeyboard", ConsoleProxy.keyboardType == ConsoleProxy.KEYBOARD_RAW ? "true" : "false"); + + StringWriter writer = new StringWriter(); + try { + ConsoleProxy.processTemplate("viewer.ftl", model, writer); + } catch (IOException e) { + s_logger.warn("Unexpected exception in processing template.", e); + } catch (TemplateException e) { + s_logger.warn("Unexpected exception in processing template.", e); + } + StringBuffer sb = writer.getBuffer(); + if(s_logger.isTraceEnabled()) + s_logger.trace("onAjaxClientStart response: " + sb.toString()); + return sb.toString(); + */ + return getAjaxViewerPageContent(sbTileSequence.toString(), imgUrl, + updateUrl, width, height, tileWidth, tileHeight, title, + ConsoleProxy.keyboardType == ConsoleProxy.KEYBOARD_RAW); + } + + private String getAjaxViewerPageContent(String tileSequence, String imgUrl, String updateUrl, int width, + int height, int tileWidth, int tileHeight, String title, boolean rawKeyboard) { + + String[] content = new String[] { + "", + "", + "", + "", + "", + "", + "", + "", + "" + title + "", + "", + "", + "
", + "", + "", + "
", + "
", + "", + "", + "" + }; + + StringBuffer sb = new StringBuffer(); + for(int i = 0; i < content.length; i++) + sb.append(content[i]); + + return sb.toString(); + } + + public String onAjaxClientDisconnected() { + return "onDisconnect();"; + } + + public String onAjaxClientUpdate() { + if(!waitForViewerReady()) + return onAjaxClientDisconnected(); + + synchronized(tileDirtyEvent) { + if(!dirtyFlag) { + try { + tileDirtyEvent.wait(3000); + } catch(InterruptedException e) { + } + } + } + + boolean doResize = false; + synchronized(this) { + if(framebufferResized) { + framebufferResized = false; + doResize = true; + } + } + + List tiles; + + if(doResize) + tiles = tracker.scan(true); + else + tiles = tracker.scan(false); + dirtyFlag = false; + + String imgUrl = prepareAjaxImage(tiles, false); + StringBuffer sbTileSequence = new StringBuffer(); + int i = 0; + for(TileInfo tile : tiles) { + sbTileSequence.append("[").append(tile.getRow()).append(",").append(tile.getCol()).append("]"); + if(i < tiles.size() - 1) + sbTileSequence.append(","); + + i++; + } + +/* + SimpleHash model = new SimpleHash(); + model.put("tileSequence", sbTileSequence.toString()); + model.put("resized", doResize); + model.put("imgUrl", imgUrl); + model.put("width", String.valueOf(resizedFramebufferWidth)); + model.put("height", String.valueOf(resizedFramebufferHeight)); + model.put("tileWidth", String.valueOf(tracker.getTileWidth())); + model.put("tileHeight", String.valueOf(tracker.getTileHeight())); + + StringWriter writer = new StringWriter(); + try { + ConsoleProxy.processTemplate("viewer-update.ftl", model, writer); + } catch (IOException e) { + s_logger.warn("Unexpected exception in processing template.", e); + } catch (TemplateException e) { + s_logger.warn("Unexpected exception in processing template.", e); + } + StringBuffer sb = writer.getBuffer(); + + if(s_logger.isTraceEnabled()) + s_logger.trace("onAjaxClientUpdate response: " + sb.toString()); + + return sb.toString(); +*/ + return getAjaxViewerUpdatePageContent(sbTileSequence.toString(), imgUrl, doResize, resizedFramebufferWidth, + resizedFramebufferHeight, tracker.getTileWidth(), tracker.getTileHeight()); + } + + private String getAjaxViewerUpdatePageContent(String tileSequence, String imgUrl, boolean resized, int width, + int height, int tileWidth, int tileHeight) { + + String[] content = new String[] { + "tileMap = [ " + tileSequence + " ];", + resized ? "ajaxViewer.resize('main_panel', " + width + ", " + height + " , " + tileWidth + ", " + tileHeight + ");" : "", + "ajaxViewer.refresh('" + imgUrl + "', tileMap, false);" + }; + + StringBuffer sb = new StringBuffer(); + for(int i = 0; i < content.length; i++) + sb.append(content[i]); + + return sb.toString(); + } + + + public long getAjaxSessionId() { + return this.ajaxSessionId; + } + + public AjaxFIFOImageCache getAjaxImageCache() { + return ajaxImageCache; + } + + public boolean isAjaxViewer() { + return ajaxViewer; + } + + public synchronized void setAjaxViewer(boolean ajaxViewer) { + if(this.ajaxViewer != ajaxViewer) { + if(this.ajaxViewer) { + // previous session was AJAX session + this.ajaxSessionId++; // increase the session id so that it will disconnect existing AJAX viewer + } else { + // close java client session + if(clientStream != null) { + byte[] bs = new byte[2]; + bs[0] = (byte)250; + bs[1] = 1; + writeToClientStream(bs); + + try { + clientStream.close(); + } catch (IOException e) { + } + clientStream = null; + } + } + this.ajaxViewer = ajaxViewer; + } + } + + public void writeServer(byte[] b, int off, int len) { + synchronized (this) { + if (!rfb.closed()) { + try { + // We lock the viewer to avoid race condition when connecting one + // client forces the current client to disconnect. + rfb.os.write(b, off, len); + rfb.os.flush(); + } catch (IOException e) { + // Swallow the exception because we want the client connection to sustain + // even when server connection is severed and reestablished. + s_logger.info("Ignore exception when writing to server: " + e); + rfb.close(); + } + } else { + s_logger.info("Dropping client event because server connection is closed "); + } + } + } + + public void sendClientMouseEvent(int event, int x, int y, int code, int modifiers) { + if(code == 2) + modifiers |= MouseEvent.BUTTON3_MASK; + else + modifiers |= MouseEvent.BUTTON1_MASK; + + int id = 0; + if(event == 1) + id = MouseEvent.MOUSE_MOVED; + else if(event == 2) + id = MouseEvent.MOUSE_PRESSED; + else if(event == 3) + id = MouseEvent.MOUSE_RELEASED; + else if(event == 8) + id = MouseEvent.MOUSE_PRESSED; + + long curTicks = System.currentTimeMillis(); + MouseEvent mouseEvent = new MouseEvent(vc, id, + curTicks, modifiers, x, y, 1, false); + + synchronized (this) { + if (rfb != null && !rfb.closed()) { + try { + rfb.writePointerEvent(mouseEvent); + if(event == 8) { + if(s_logger.isTraceEnabled()) + s_logger.trace("Replay mouse double click event at " + x + "," + y); + + mouseEvent = new MouseEvent(vc, MouseEvent.MOUSE_RELEASED, + curTicks, modifiers, x, y, 1, false); + rfb.writePointerEvent(mouseEvent); + + mouseEvent = new MouseEvent(vc, MouseEvent.MOUSE_PRESSED, + curTicks, modifiers, x, y, 1, false); + rfb.writePointerEvent(mouseEvent); + + mouseEvent = new MouseEvent(vc, MouseEvent.MOUSE_RELEASED, + curTicks, modifiers, x, y, 1, false); + rfb.writePointerEvent(mouseEvent); + } + } catch (IOException e) { + s_logger.warn("Exception while sending mouse event. ", e); + } + } + } + } + + public void sendClientRawKeyboardEvent(int event, int code, int modifiers) { + code = ConsoleProxyAjaxKeyMapper.getInstance().getJvmKeyCode(code); + switch(event) { + case 4 : // Key press + // + // special handling for ' and " (keycode: 222, char code : 39 and 34 + // + if(code == 39 || code == 34) { + writeKeyboardEvent(KeyEvent.KEY_PRESSED, 222, (char)code, getAwtModifiers(modifiers)); + } + break; + + case 5 : // Key down + if((modifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0 && (modifiers & ConsoleProxyViewer.ALT_KEY_MASK) != 0 && code == KeyEvent.VK_INSERT) { + code = KeyEvent.VK_DELETE; + } + + if(code != 222) { + writeKeyboardEvent(KeyEvent.KEY_PRESSED, code, + ConsoleProxyAjaxKeyMapper.getInstance().shiftedKeyCharFromKeyCode(code, (modifiers & ConsoleProxyViewer.SHIFT_KEY_MASK) != 0), + getAwtModifiers(modifiers)); + } + break; + + case 6 : // Key Up + writeKeyboardEvent(KeyEvent.KEY_RELEASED, code, + ConsoleProxyAjaxKeyMapper.getInstance().shiftedKeyCharFromKeyCode(code, (modifiers & ConsoleProxyViewer.SHIFT_KEY_MASK) != 0), + getAwtModifiers(modifiers)); + break; + } + } + + public void sendClientKeyboardEvent(int event, int code, int modifiers) { + int vkCode; + switch(event) { + case 4 : // Key press + if(code == 0 || (modifiers & (ConsoleProxyViewer.CTRL_KEY_MASK | ConsoleProxyViewer.META_KEY_MASK | ConsoleProxyViewer.ALT_KEY_MASK)) != 0) { + // if code is extend keys or has ctrl, alt, meta being pressed, ignore javascript key-press event + return; + } + + vkCode = ConsoleProxyAjaxKeyMapper.getInstance().getRegularCharVkCode(code); + if(vkCode > 0) { + writeKeyboardEvent(KeyEvent.KEY_PRESSED, vkCode, (char)code, 0); + writeKeyboardEvent(KeyEvent.KEY_RELEASED, vkCode, (char)code, 0); + } + break; + + case 5 : // Key down + vkCode = ConsoleProxyAjaxKeyMapper.getInstance().getActionCharVkCode(code); + if(vkCode >= 0 || (modifiers & (ConsoleProxyViewer.CTRL_KEY_MASK | ConsoleProxyViewer.META_KEY_MASK | ConsoleProxyViewer.ALT_KEY_MASK)) != 0) { + if(vkCode < 0) { + vkCode = ConsoleProxyAjaxKeyMapper.getInstance().getRegularCharVkCode(code); + + if((modifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0) { // if control-key is pressed, always use lower-case char-code + if(vkCode >= (int)'A' && vkCode <= (int)'Z') + vkCode = (int)'a' + (vkCode - (int)'A'); + } + } + + if((modifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0 && (modifiers & ConsoleProxyViewer.ALT_KEY_MASK) != 0 && vkCode == KeyEvent.VK_INSERT) { + vkCode = KeyEvent.VK_DELETE; + } + + writeKeyboardEvent(KeyEvent.KEY_PRESSED, vkCode, (char)vkCode, + getAwtModifiers(modifiers)); + } + break; + + case 6 : // Key Up + vkCode = ConsoleProxyAjaxKeyMapper.getInstance().getActionCharVkCode(code); + if(vkCode >= 0 || (modifiers & (ConsoleProxyViewer.CTRL_KEY_MASK | ConsoleProxyViewer.META_KEY_MASK | ConsoleProxyViewer.ALT_KEY_MASK)) != 0) { + if(vkCode < 0) { + vkCode = ConsoleProxyAjaxKeyMapper.getInstance().getRegularCharVkCode(code); + + if((modifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0) { // if control-key is pressed, always use lower-case char-code + if(vkCode >= (int)'A' && vkCode <= (int)'Z') + vkCode = (int)'a' + (vkCode - (int)'A'); + } + + if((modifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0 && (modifiers & ConsoleProxyViewer.ALT_KEY_MASK) != 0 && vkCode == KeyEvent.VK_INSERT) { + vkCode = KeyEvent.VK_DELETE; + } + } + + writeKeyboardEvent(KeyEvent.KEY_RELEASED, vkCode, (char)vkCode, + getAwtModifiers(modifiers)); + } + break; + } + } + + private static int getAwtModifiers(int jsModifiers) { + int awtModifiers = 0; + + if((jsModifiers & ConsoleProxyViewer.SHIFT_KEY_MASK) != 0) { + awtModifiers |= InputEvent.SHIFT_DOWN_MASK; + } + + if((jsModifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0) { + awtModifiers |= InputEvent.CTRL_DOWN_MASK; + } + + if((jsModifiers & ConsoleProxyViewer.ALT_KEY_MASK) != 0) { + awtModifiers |= InputEvent.ALT_DOWN_MASK; + } + + return awtModifiers; + } + + private void writeKeyboardEvent(int keyEventType, int code, char keyChar, int modifiers) { + KeyEvent keyEvent; + try { + keyEvent = new KeyEvent(vc, keyEventType, + System.currentTimeMillis(), modifiers, code, keyChar); + } catch(Exception e) { + s_logger.warn("Unable to construct KeyEvent object, key code: " + code + ", keyChar: " + keyChar + " ", e); + return; + } + + synchronized (this) { + if (rfb != null && !rfb.closed()) { + try { + rfb.writeKeyEvent(keyEvent); + } catch (IOException e) { + s_logger.warn("Exception while sending keyboard event. ", e); + } + } + } + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/ViewerOptions.java b/console-proxy/src/com/cloud/consoleproxy/ViewerOptions.java new file mode 100644 index 00000000000..4ad8930e6f3 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ViewerOptions.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +public class ViewerOptions { + public int preferredEncoding = -1; + public int compressLevel = 5; + public int jpegQuality = 5; + public boolean useCopyRect = false; + public boolean requestCursorUpdates = true; + public boolean ignoreCursorUpdates = false; + + public boolean eightBitColors = false; + + public boolean reverseMouseButtons2And3 = false; + public boolean shareDesktop = true; + public boolean viewOnly = false; + public int scaleCursor = 0; + + public boolean autoScale = false; + public int scalingFactor = 100; +} diff --git a/console-proxy/ui/viewer-bad-sid.ftl b/console-proxy/ui/viewer-bad-sid.ftl new file mode 100644 index 00000000000..26c08a690ae --- /dev/null +++ b/console-proxy/ui/viewer-bad-sid.ftl @@ -0,0 +1,11 @@ + + + + + +
+

Unable to start console session as access is denied because of bad sid

+
+ + + diff --git a/console-proxy/ui/viewer-connect-failed.ftl b/console-proxy/ui/viewer-connect-failed.ftl new file mode 100644 index 00000000000..af3a8b8d928 --- /dev/null +++ b/console-proxy/ui/viewer-connect-failed.ftl @@ -0,0 +1,11 @@ + + + + + +
+

Unable to start console session as connection is refused by the machine you are accessing

+
+ + + diff --git a/console-proxy/ui/viewer-update.ftl b/console-proxy/ui/viewer-update.ftl new file mode 100644 index 00000000000..2217a136bcf --- /dev/null +++ b/console-proxy/ui/viewer-update.ftl @@ -0,0 +1,6 @@ +tileMap = [ ${tileSequence} ]; +<#if resized == true> + ajaxViewer.resize('main_panel', ${width}, ${height}, ${tileWidth}, ${tileHeight}); + +ajaxViewer.refresh('${imgUrl}', tileMap, false); + \ No newline at end of file diff --git a/console-proxy/ui/viewer.ftl b/console-proxy/ui/viewer.ftl new file mode 100644 index 00000000000..dc15fe9933f --- /dev/null +++ b/console-proxy/ui/viewer.ftl @@ -0,0 +1,41 @@ + + + + + + +${title} + + + + +
+ + + + + diff --git a/console-proxy/vm-script/vmops b/console-proxy/vm-script/vmops new file mode 100644 index 00000000000..3d68d1b7583 --- /dev/null +++ b/console-proxy/vm-script/vmops @@ -0,0 +1,101 @@ +#!/bin/bash +# +# vmops Script to start and stop the VMOps Agent. +# +# Author: Chiradeep Vittal +# chkconfig: 2345 99 01 +# description: Start up the VMOps agent + +# Source function library. +if [ -f /etc/init.d/functions ] +then + . /etc/init.d/functions +fi + +_success() { + if [ -f /etc/init.d/functions ] + then + success + else + echo "Success" + fi +} + +_failure() { + if [ -f /etc/init.d/functions ] + then + failure + else + echo "Failed" + fi +} +RETVAL=$? +VMOPS_HOME="/usr/local/vmops" + +mkdir -p /var/log/vmops + +get_pids() { + local i + for i in $(ps -ef| grep java | grep -v grep | awk '{print $2}'); + do + echo $(pwdx $i) | grep "$VMOPS_HOME" | grep -i console | awk -F: '{print $1}'; + done +} + +start() { + local pid=$(get_pids) + echo -n "Starting VMOps Console Proxy: " + if [ -f $VMOPS_HOME/consoleproxy/run.sh ]; + then + if [ "$pid" == "" ] + then + (cd $VMOPS_HOME/consoleproxy; nohup ./run.sh > /var/log/vmops/vmops.out 2>&1 & ) + pid=$(get_pids) + echo $pid > /var/run/vmops.pid + fi + _success + else + _failure + fi + echo +} + +stop() { + local pid + echo -n "Stopping VMOps agent: " + for pid in $(get_pids) + do + kill $pid + done + _success + echo +} + +status() { + local pids=$(get_pids) + if [ "$pids" == "" ] + then + echo "VMOps agent is not running" + return 1 + fi + echo "VMOps agent is running: process id: $pids" + return 0 +} + + +case "$1" in + start) start + ;; + stop) stop + ;; + status) status + ;; + restart) stop + start + ;; + *) echo $"Usage: $0 {start|stop|status|restart}" + exit 1 + ;; +esac + +exit $RETVAL diff --git a/console-viewer/.classpath b/console-viewer/.classpath new file mode 100644 index 00000000000..19542e73fc0 --- /dev/null +++ b/console-viewer/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/console-viewer/.project b/console-viewer/.project new file mode 100644 index 00000000000..1b47b7baad3 --- /dev/null +++ b/console-viewer/.project @@ -0,0 +1,17 @@ + + + console-viewer + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/console-viewer/scripts/run-viewer.bat b/console-viewer/scripts/run-viewer.bat new file mode 100644 index 00000000000..f35990a19c3 --- /dev/null +++ b/console-viewer/scripts/run-viewer.bat @@ -0,0 +1,5 @@ +REM Example Usage +REM run-viewer HOST 192.168.1.220 PORT 5917 PROXYHOST 127-0-0-1.realhostip.com PROXYPORT 5999 SID pass +REM + +java -cp applet\VMOpsConsoleApplet.jar com.vmops.consoleviewer.ConsoleApplet %* diff --git a/console-viewer/src/com/cloud/consoleviewer/AuthPanel.java b/console-viewer/src/com/cloud/consoleviewer/AuthPanel.java new file mode 100644 index 00000000000..416d8b5065f --- /dev/null +++ b/console-viewer/src/com/cloud/consoleviewer/AuthPanel.java @@ -0,0 +1,118 @@ +// +// Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. +// Copyright (C) 2002-2006 Constantin Kaplinsky. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +package com.cloud.consoleviewer; + +import java.awt.*; +import java.awt.event.*; + +// +// The panel which implements the user authentication scheme +// +@SuppressWarnings("serial") +class AuthPanel extends Panel implements ActionListener { + + TextField passwordField; + Button okButton; + + // + // Constructor. + // + + public AuthPanel(ConsoleViewer viewer) + { + Label titleLabel = new Label("VNC Authentication", Label.CENTER); + titleLabel.setFont(new Font("Helvetica", Font.BOLD, 18)); + + Label promptLabel = new Label("Password:", Label.CENTER); + + passwordField = new TextField(10); + passwordField.setForeground(Color.black); + passwordField.setBackground(Color.white); + passwordField.setEchoChar('*'); + + okButton = new Button("OK"); + + GridBagLayout gridbag = new GridBagLayout(); + GridBagConstraints gbc = new GridBagConstraints(); + + setLayout(gridbag); + + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(0,0,20,0); + gridbag.setConstraints(titleLabel,gbc); + add(titleLabel); + + gbc.fill = GridBagConstraints.NONE; + gbc.gridwidth = 1; + gbc.insets = new Insets(0,0,0,0); + gridbag.setConstraints(promptLabel,gbc); + add(promptLabel); + + gridbag.setConstraints(passwordField,gbc); + add(passwordField); + passwordField.addActionListener(this); + + // gbc.ipady = 10; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.fill = GridBagConstraints.BOTH; + gbc.insets = new Insets(0,20,0,0); + gbc.ipadx = 30; + gridbag.setConstraints(okButton,gbc); + add(okButton); + okButton.addActionListener(this); + } + + // + // Move keyboard focus to the default object, that is, the password + // text field. + // + + public void moveFocusToDefaultField() + { + passwordField.requestFocus(); + } + + // + // This method is called when a button is pressed or return is + // pressed in the password text field. + // + + public synchronized void actionPerformed(ActionEvent evt) + { + if (evt.getSource() == passwordField || evt.getSource() == okButton) { + passwordField.setEnabled(false); + notify(); + } + } + + // + // Wait for user entering a password, and return it as String. + // + + public synchronized String getPassword() throws Exception + { + try { + wait(); + } catch (InterruptedException e) { } + return passwordField.getText(); + } + +} diff --git a/console-viewer/src/com/cloud/consoleviewer/ButtonPanel.java b/console-viewer/src/com/cloud/consoleviewer/ButtonPanel.java new file mode 100644 index 00000000000..d5cf61efa55 --- /dev/null +++ b/console-viewer/src/com/cloud/consoleviewer/ButtonPanel.java @@ -0,0 +1,164 @@ +// +// Copyright (C) 2001,2002 HorizonLive.com, Inc. All Rights Reserved. +// Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// +// ButtonPanel class implements panel with four buttons in the +// VNCViewer desktop window. +// + +package com.cloud.consoleviewer; + +import java.awt.*; +import java.awt.event.*; +import java.io.*; + +import com.cloud.console.RfbProto; +@SuppressWarnings("serial") +class ButtonPanel extends Panel implements ActionListener { + + ConsoleViewer viewer; + Button disconnectButton; + Button optionsButton; + Button recordButton; + Button clipboardButton; + Button ctrlAltDelButton; + Button refreshButton; + + ButtonPanel(ConsoleViewer v) { + viewer = v; + + setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); +/* + disconnectButton = new Button("Disconnect"); + disconnectButton.setEnabled(false); + add(disconnectButton); + disconnectButton.addActionListener(this); + optionsButton = new Button("Options"); + add(optionsButton); + optionsButton.addActionListener(this); + clipboardButton = new Button("Clipboard"); + clipboardButton.setEnabled(false); + add(clipboardButton); + clipboardButton.addActionListener(this); + if (viewer.rec != null) { + recordButton = new Button("Record"); + add(recordButton); + recordButton.addActionListener(this); + } +*/ + ctrlAltDelButton = new Button("Send Ctrl-Alt-Del"); + ctrlAltDelButton.setEnabled(false); + add(ctrlAltDelButton); + ctrlAltDelButton.addActionListener(this); + refreshButton = new Button("Refresh"); + refreshButton.setEnabled(false); + add(refreshButton); + refreshButton.addActionListener(this); + } + + // + // Enable buttons on successful connection. + // + + public void enableButtons() { + /* + disconnectButton.setEnabled(true); + clipboardButton.setEnabled(true); + */ + refreshButton.setEnabled(true); + } + + // + // Disable all buttons on disconnect. + // + + public void disableButtonsOnDisconnect() { +/* + remove(disconnectButton); + disconnectButton = new Button("Hide desktop"); + disconnectButton.setEnabled(true); + add(disconnectButton, 0); + disconnectButton.addActionListener(this); + + optionsButton.setEnabled(false); + clipboardButton.setEnabled(false); + */ + ctrlAltDelButton.setEnabled(false); + refreshButton.setEnabled(false); + + validate(); + } + + // + // Enable/disable controls that should not be available in view-only + // mode. + // + + public void enableRemoteAccessControls(boolean enable) { + ctrlAltDelButton.setEnabled(enable); + } + + // + // Event processing. + // + @SuppressWarnings("deprecation") + public void actionPerformed(ActionEvent evt) { + + viewer.moveFocusToDesktop(); + + if (evt.getSource() == disconnectButton) { + viewer.disconnect(); + + } else if (evt.getSource() == optionsButton) { +// viewer.options.setVisible(!viewer.options.isVisible()); + + } else if (evt.getSource() == recordButton) { +// viewer.rec.setVisible(!viewer.rec.isVisible()); + + } else if (evt.getSource() == clipboardButton) { +// viewer.clipboard.setVisible(!viewer.clipboard.isVisible()); + + } else if (evt.getSource() == ctrlAltDelButton) { + try { + final int modifiers = InputEvent.CTRL_MASK | InputEvent.ALT_MASK; + + KeyEvent ctrlAltDelEvent = + new KeyEvent(this, KeyEvent.KEY_PRESSED, 0, modifiers, 127); + viewer.rfb.writeKeyEvent(ctrlAltDelEvent); + + ctrlAltDelEvent = + new KeyEvent(this, KeyEvent.KEY_RELEASED, 0, modifiers, 127); + viewer.rfb.writeKeyEvent(ctrlAltDelEvent); + + } catch (IOException e) { + e.printStackTrace(); + } + } else if (evt.getSource() == refreshButton) { + try { + RfbProto rfb = viewer.rfb; + rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth, + rfb.framebufferHeight, false); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} + diff --git a/console-viewer/src/com/cloud/consoleviewer/ClipboardFrame.java b/console-viewer/src/com/cloud/consoleviewer/ClipboardFrame.java new file mode 100644 index 00000000000..0f40da569bd --- /dev/null +++ b/console-viewer/src/com/cloud/consoleviewer/ClipboardFrame.java @@ -0,0 +1,135 @@ +// +// Copyright (C) 2001 HorizonLive.com, Inc. All Rights Reserved. +// Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +package com.cloud.consoleviewer; + +// +// Clipboard frame. +// + +import java.awt.*; +import java.awt.event.*; +@SuppressWarnings("serial") +class ClipboardFrame extends Frame + implements WindowListener, ActionListener { + + TextArea textArea; + Button clearButton, closeButton; + String selection; + ConsoleViewer viewer; + + // + // Constructor. + // + + ClipboardFrame(ConsoleViewer v) { + super("TightVNC Clipboard"); + + viewer = v; + + GridBagLayout gridbag = new GridBagLayout(); + setLayout(gridbag); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.fill = GridBagConstraints.BOTH; + gbc.weighty = 1.0; + + textArea = new TextArea(5, 40); + gridbag.setConstraints(textArea, gbc); + add(textArea); + + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.weighty = 0.0; + gbc.gridwidth = 1; + + clearButton = new Button("Clear"); + gridbag.setConstraints(clearButton, gbc); + add(clearButton); + clearButton.addActionListener(this); + + closeButton = new Button("Close"); + gridbag.setConstraints(closeButton, gbc); + add(closeButton); + closeButton.addActionListener(this); + + pack(); + + addWindowListener(this); + } + + + // + // Set the cut text from the RFB server. + // + + void setCutText(String text) { + selection = text; + textArea.setText(text); + if (isVisible()) { + textArea.selectAll(); + } + } + + + // + // When the focus leaves the window, see if we have new cut text and + // if so send it to the RFB server. + // + + public void windowDeactivated (WindowEvent evt) { + if (selection != null && !selection.equals(textArea.getText())) { + selection = textArea.getText(); + viewer.setCutText(selection); + } + } + + // + // Close our window properly. + // + + public void windowClosing(WindowEvent evt) { + setVisible(false); + } + + // + // Ignore window events we're not interested in. + // + + public void windowActivated(WindowEvent evt) {} + public void windowOpened(WindowEvent evt) {} + public void windowClosed(WindowEvent evt) {} + public void windowIconified(WindowEvent evt) {} + public void windowDeiconified(WindowEvent evt) {} + + + // + // Respond to button presses + // + + public void actionPerformed(ActionEvent evt) { + if (evt.getSource() == clearButton) { + textArea.setText(""); + } else if (evt.getSource() == closeButton) { + setVisible(false); + } + } +} diff --git a/console-viewer/src/com/cloud/consoleviewer/ConsoleApplet.java b/console-viewer/src/com/cloud/consoleviewer/ConsoleApplet.java new file mode 100644 index 00000000000..f7024512ce1 --- /dev/null +++ b/console-viewer/src/com/cloud/consoleviewer/ConsoleApplet.java @@ -0,0 +1,167 @@ + +// +// Copyright (C) 2001-2004 HorizonLive.com, Inc. All Rights Reserved. +// Copyright (C) 2002 Constantin Kaplinsky. All Rights Reserved. +// Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// +// VncViewer.java - the VNC viewer applet. This class mainly just sets up the +// user interface, leaving it to the VncCanvas to do the actual rendering of +// a VNC desktop. +// + +package com.cloud.consoleviewer; + +import java.awt.*; +import java.awt.event.*; + +import com.cloud.console.Logger; + +public class ConsoleApplet extends java.applet.Applet implements WindowListener { + private static final Logger s_logger = Logger.getLogger(ConsoleApplet.class); + + private static final long serialVersionUID = -8463170916581351766L; + + ConsoleViewer viewer; + String errorMsg; + + public ConsoleApplet() { + viewer = new ConsoleViewer(); + } + + public static void main(String[] argv) { + ConsoleApplet applet = new ConsoleApplet(); + applet.viewer.mainArgs = argv; + applet.viewer.inAnApplet = false; + applet.viewer.inSeparateFrame = true; + applet.viewer.inProxyMode = false; + applet.init(); + applet.start(); + } + + // Reference to this applet for inter-applet communication. + public static java.applet.Applet refApplet; + + // + // init() + // + + public void init() { + s_logger.info("Initializing applet"); + refApplet = this; + viewer.applet = this; + viewer.init(); + disableFocusTraversal(this); + errorMsg = "Connecting..."; + invalidate(); + } + + public void paint(Graphics g) { + if(viewer != null && viewer.vc != null && viewer.vc.memGraphics != null) + g.setColor(Color.WHITE); + else + g.setColor(Color.BLACK); + g.fillRect(0, 0, 800, 600); + + if(errorMsg != null && errorMsg.length() > 0) { + g.setFont(new Font(null, Font.PLAIN, 20)); + g.setColor(Color.WHITE); + FontMetrics fm = g.getFontMetrics(); + int width = fm.stringWidth(errorMsg); + int startx = (800 - width) / 2; + if (startx < 0) startx = 0; + g.drawString(errorMsg, startx, 600 / 2); + } + } + + public void paintErrorString(String msg) { + s_logger.info("paintErrorString"); + + errorMsg = msg; + invalidate(); + repaint(); + } + + public void stop() { + s_logger.info("Stopping applet"); + viewer.stop(); + } + + // + // This method is called before the applet is destroyed. + // + public void destroy() { + s_logger.info("Destroying applet"); + viewer.destroy(); + viewer = null; + } + + public void sendCtrlAltDel() { + if(viewer != null && viewer.vc != null) + viewer.vc.sendCtrlAltDel(); + } + + public int getFrameBufferWidth() { + if(viewer != null && viewer.vc != null) + return viewer.vc.getWidth(); + + return 800; + } + + public int getFrameBufferHeight() { + if(viewer != null && viewer.vc != null) + return viewer.vc.getHeight(); + return 600; + } + + // + // Close application properly on window close event. + // + + public void windowClosing(WindowEvent evt) { + s_logger.info("Closing window"); + viewer.windowClosing(evt); + } + + // + // Ignore window events we're not interested in. + // + public void windowActivated(WindowEvent evt) { + } + + public void windowDeactivated(WindowEvent evt) { + } + + public void windowOpened(WindowEvent evt) { + } + + public void windowClosed(WindowEvent evt) { + } + + public void windowIconified(WindowEvent evt) { + } + + public void windowDeiconified(WindowEvent evt) { + } + + static public void disableFocusTraversal(Container con) { + con.setFocusTraversalKeysEnabled(false); + con.setFocusCycleRoot(true); + } +} diff --git a/console-viewer/src/com/cloud/consoleviewer/ConsoleViewer.java b/console-viewer/src/com/cloud/consoleviewer/ConsoleViewer.java new file mode 100644 index 00000000000..488778de179 --- /dev/null +++ b/console-viewer/src/com/cloud/consoleviewer/ConsoleViewer.java @@ -0,0 +1,1247 @@ +// +// Copyright (C) 2001-2004 HorizonLive.com, Inc. All Rights Reserved. +// Copyright (C) 2002 Constantin Kaplinsky. All Rights Reserved. +// Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// +// VncViewer.java - the VNC viewer applet. This class mainly just sets up the +// user interface, leaving it to the VncCanvas to do the actual rendering of +// a VNC desktop. +// + +package com.cloud.consoleviewer; + +import java.applet.Applet; +import java.awt.*; +import java.awt.event.*; +import java.awt.image.BufferedImage; +import java.awt.image.MemoryImageSource; +import java.io.*; +import java.net.*; +import java.util.Date; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; + +import com.cloud.console.AuthenticationException; +import com.cloud.console.ConsoleCanvas; +import com.cloud.console.ConsoleCanvas2; +import com.cloud.console.Logger; +import com.cloud.console.RfbProto; +import com.cloud.console.RfbProtoAdapter; +import com.cloud.console.RfbViewer; + +@SuppressWarnings("deprecation") +public class ConsoleViewer implements java.lang.Runnable, RfbViewer, RfbProtoAdapter { + private static final Logger s_logger = Logger.getLogger(ConsoleViewer.class); + + int id = getNextId(); + boolean compressServerMessage = false; + long createTime = System.currentTimeMillis(); + long lastUsedTime = System.currentTimeMillis(); + + boolean dropMe = false; + + OutputStream clientStream; + String clientStreamInfo; + + int status; + public final static int STATUS_ERROR = -1; + public final static int STATUS_UNINITIALIZED = 0; + public final static int STATUS_CONNECTING = 1; + public final static int STATUS_INITIALIZING = 2; + public final static int STATUS_NORMAL_OPERATION = 3; + public final static int STATUS_AUTHENTICATION_FAILURE = 100; + + public final static int DEFAULT_READ_TIMEOUT_SECONDS = 45; + + boolean inAnApplet = true; + boolean inSeparateFrame = false; + boolean inProxyMode = false; + + String[] mainArgs; + + RfbProto rfb; + Thread rfbThread; + + ConsoleApplet applet; + Frame vncFrame; + Container vncContainer; + ScrollPane desktopScrollPane; + GridBagLayout gridbag; + ButtonPanel buttonPanel; + Label connStatusLabel; + ConsoleCanvas vc; + OptionsFrame options; + ClipboardFrame clipboard; + private RecordingFrame rec; + + // Control session recording. + Object recordingSync; + String sessionFileName; + boolean recordingActive; + boolean recordingStatusChanged; + String cursorUpdatesDef; + String eightBitColorsDef; + + // Variables read from parameter values. + String socketFactory; + String host; + int port; + String proxyHost; + int proxyPort; + String passwordParam; + boolean showControls; + boolean offerRelogin; + boolean showOfflineDesktop; + int deferScreenUpdates; + int deferCursorUpdates; + int deferUpdateRequests; + + static int id_count = 1; + synchronized static int getNextId() { + return id_count++; + } + + // + // init() + // + + public void init() { + /* + if (inProxyMode) { + initProxy(); + return; + } + */ + readParameters(); + + if (inSeparateFrame) { + vncFrame = new Frame("VMOps ConsoleViewer"); + if (!inAnApplet) { + // vncFrame.add("Center", this); + } + vncContainer = vncFrame; + } else { + vncContainer = applet; + } + + recordingSync = new Object(); + + options = new OptionsFrame(this); + // clipboard = new ClipboardFrame(this); + // if (RecordingFrame.checkSecurity()) + // rec = new RecordingFrame(this); + + sessionFileName = null; + recordingActive = false; + recordingStatusChanged = false; + cursorUpdatesDef = null; + eightBitColorsDef = null; + + if (inSeparateFrame) + vncFrame.addWindowListener(applet); + rfbThread = new Thread(this); + rfbThread.start(); + } + + /* + private void initProxy() { + recordingSync = new Object(); + + options = new OptionsFrame(this); + options.viewOnly = true; + + sessionFileName = null; + recordingActive = false; + recordingStatusChanged = false; + cursorUpdatesDef = null; + eightBitColorsDef = null; + + rfbThread = new Thread(this); + rfbThread.setName("RFB Thread " + rfbThread.getId() + " >" + host + ":" + + port); + + rfbThread.start(); + } + */ + + public void update(Graphics g) { + } + + // + // run() - executed by the rfbThread to deal with the RFB socket. + // + + public void run() { + /* + if (inProxyMode) { + runProxy(); + return; + } + */ + + int[] pixels = new int[16 * 16]; + Image image = Toolkit.getDefaultToolkit().createImage( + new MemoryImageSource(16, 16, pixels, 0, 16)); + Cursor transparentCursor = + Toolkit.getDefaultToolkit().createCustomCursor + (image, new Point(0, 0), "invisibleCursor"); + vncContainer.setCursor(transparentCursor); + + + gridbag = new GridBagLayout(); + vncContainer.setLayout(gridbag); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.anchor = GridBagConstraints.NORTHWEST; + //gbc.anchor = GridBagConstraints.CENTER; + + if (showControls) { + buttonPanel = new ButtonPanel(this); + gridbag.setConstraints(buttonPanel, gbc); + vncContainer.add(buttonPanel); + } + + // FIXME: Use auto-scaling not only in a separate frame. + if (options.autoScale && inSeparateFrame) { + Dimension screenSize; + try { + screenSize = vncContainer.getToolkit().getScreenSize(); + } catch (Exception e) { + screenSize = new Dimension(0, 0); + } + createCanvas(screenSize.width - 32, screenSize.height - 32); + } else { + createCanvas(0, 0); + } + + if (!options.viewOnly) + vc.enableInput(true); + + gbc.weightx = 1.0; + gbc.weighty = 1.0; + + if (inSeparateFrame) { + + // Create a panel which itself is resizeable and can hold + // non-resizeable VncCanvas component at the top left corner. + Panel canvasPanel = new Panel(); + canvasPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + canvasPanel.add(vc); + + // Create a ScrollPane which will hold a panel with VncCanvas + // inside. + desktopScrollPane = new ScrollPane(ScrollPane.SCROLLBARS_AS_NEEDED); + gbc.fill = GridBagConstraints.BOTH; + gridbag.setConstraints(desktopScrollPane, gbc); + desktopScrollPane.add(canvasPanel); + // Finally, add our ScrollPane to the Frame window. + vncFrame.add(desktopScrollPane); + // vncFrame.setTitle(rfb.desktopName); + vncFrame.pack(); + vc.resizeDesktopFrame(); + } else { + gridbag.setConstraints(vc, gbc); + applet.add(vc); + applet.validate(); + } + + if (showControls) + buttonPanel.enableButtons(); + moveFocusToDesktop(); + + try { + connectAndAuthenticate(); + doProtocolInitialisation(); + vc.rfb = rfb; + vc.setPixelFormat(); + vc.rfb.writeFramebufferUpdateRequest(0, 0, vc.rfb.framebufferWidth, + vc.rfb.framebufferHeight, false); + vc.processNormalProtocol(); + // We should never get here, but just in case + showMessage("Disconnected from server"); + } catch (NoRouteToHostException e) { + fatalError("Network error: no route to server: " + host, e); + } catch (UnknownHostException e) { + fatalError("Network error: server name unknown: " + host, e); + } catch (ConnectException e) { + fatalError("Network error: could not connect to server: " + host + + ":" + port, e); + } catch (EOFException e) { + fatalError("Network error: remote side closed connection", e); + } catch (IOException e) { + fatalError(e.getMessage(), e); + } catch (Exception e) { + fatalError(e.getMessage(), e); + } finally { + encodingsSaved = null; + nEncodingsSaved = 0; + synchronized (this) { + if (rfb != null) { + rfb.close(); + } + } + } + + s_logger.info("Client RFB thread terminated"); + } + + /* + public void runProxy() { + createCanvas(0, 0); + + int delay = 0; + while (!dropMe) { + try { + status = STATUS_CONNECTING; + connectAndAuthenticate(); + delay = 0; // reset the delay interval + status = STATUS_INITIALIZING; + doProtocolInitialisation(); + vc.rfb = rfb; + vc.setPixelFormat(); + vc.rfb.writeFramebufferUpdateRequest(0, 0, + vc.rfb.framebufferWidth, vc.rfb.framebufferHeight, + false); + status = STATUS_NORMAL_OPERATION; + vc.processNormalProtocol(); + } catch (AuthenticationException e) { + status = STATUS_AUTHENTICATION_FAILURE; + String msg = e.getMessage(); + s_logger.info(msg); + } catch (Exception e) { + status = STATUS_ERROR; + s_logger.info(e.toString()); + } finally { + String oldName = Thread.currentThread().getName(); + encodingsSaved = null; + nEncodingsSaved = 0; + synchronized (this) { + if (rfb != null) { + rfb.close(); + } + } + } + if (dropMe) { + break; + } + if (status == STATUS_AUTHENTICATION_FAILURE) { + break; + } else { + s_logger.info("Exception caught, retrying in " + + delay + "ms"); + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + // ignored + } + delay = (int) ((float) (delay + 700) * 1.5); + if (delay > 3000) { + break; + } + } + } + s_logger.info("RFB thread terminating"); + } + */ + + // + // Create a VncCanvas instance. + // + + void createCanvas(int maxWidth, int maxHeight) { + /* + * // Determine if Java 2D API is available and use a special // version + * of VncCanvas if it is present. vc = null; try { // This throws + * ClassNotFoundException if there is no Java 2D API. Class cl = + * Class.forName("java.awt.Graphics2D"); // If we could load Graphics2D + * class, then we can use VncCanvas2D. cl = Class.forName("VncCanvas2"); + * Class[] argClasses = { this.getClass(), Integer.TYPE, Integer.TYPE }; + * java.lang.reflect.Constructor cstr = cl.getConstructor(argClasses); + * Object[] argObjects = { this, new Integer(maxWidth), new + * Integer(maxHeight) }; vc = (VncCanvas)cstr.newInstance(argObjects); } + * catch (Exception e) { Logger.log(Logger.INFO, + * "Warning: Java 2D API is not available"); } + * + * // If we failed to create VncCanvas2D, use old VncCanvas. if (vc == + * null) + */ + vc = new ConsoleCanvas2(this, maxWidth, maxHeight); + } + + /* + * // // Process RFB socket messages. // If the rfbThread is being stopped, + * ignore any exceptions, // otherwise rethrow the exception so it can be + * handled. // + * + * void processNormalProtocol() throws Exception { try { + * vc.processNormalProtocol(); } catch (Exception e) { if (rfbThread == + * null) { Logger.log(Logger.INFO, "Ignoring RFB socket exceptions" + + * " because applet is stopping"); } else { throw e; } } } + */ + // + // Connect to the RFB server and authenticate the user. + // + void connectAndAuthenticate() throws Exception { + showConnectionStatus("Initializing..."); + if (!inProxyMode) { + if (inSeparateFrame) { + vncFrame.pack(); + vncFrame.show(); + } else { + applet.validate(); + } + } + + if (proxyHost != null) { + showConnectionStatus("Connecting to " + proxyHost + ", port " + + proxyPort + "..."); + rfb = new RfbProto(proxyHost, proxyPort, this); + rfb.readProxyVersion(); + rfb.writeProxyString(host, port, + (passwordParam != null) ? passwordParam : ""); + } else { + showConnectionStatus("Connecting to " + host + ", port " + port + + "..."); + rfb = new RfbProto(host, port, this); + } + showConnectionStatus("Connected to server"); + + rfb.readVersionMsg(); + showConnectionStatus("RFB server supports protocol version " + + rfb.serverMajor + "." + rfb.serverMinor); + + rfb.writeVersionMsg(); + showConnectionStatus("Using RFB protocol version " + rfb.clientMajor + + "." + rfb.clientMinor); + + int secType = rfb.negotiateSecurity(); + int authType; + if (secType == RfbProto.SecTypeTight) { + showConnectionStatus("Enabling TightVNC protocol extensions"); + rfb.initCapabilities(); + rfb.setupTunneling(); + authType = rfb.negotiateAuthenticationTight(); + } else { + authType = secType; + } + + switch (authType) { + case RfbProto.AuthNone: + showConnectionStatus("No authentication needed"); + rfb.authenticateNone(); + break; + case RfbProto.AuthVNC: + showConnectionStatus("Performing standard VNC authentication"); + if (passwordParam != null) { + rfb.authenticateVNC(passwordParam); + } else { + throw new AuthenticationException("Bad password"); + /* + * String pw = askPassword(); rfb.authenticateVNC(pw); + */ + } + break; + default: + throw new Exception("Unknown authentication scheme " + authType); + } + } + + // + // Show a message describing the connection status. + // To hide the connection status label, use (msg == null). + // + + void showConnectionStatus(String msg) { + /* + if (inProxyMode) { + if (msg != null) { + s_logger.info(msg); + } + return; + } + */ + + /* + * if (msg == null) { if (vncContainer.isAncestorOf(connStatusLabel)) { + * vncContainer.remove(connStatusLabel); } return; } + * + * Logger.log(Logger.INFO, msg); + * + * if (connStatusLabel == null) { connStatusLabel = new Label("Status: " + * + msg); connStatusLabel.setFont(new Font("Helvetica", Font.PLAIN, + * 12)); } else { connStatusLabel.setText("Status: " + msg); } + * + * if (!vncContainer.isAncestorOf(connStatusLabel)) { GridBagConstraints + * gbc = new GridBagConstraints(); gbc.gridwidth = + * GridBagConstraints.REMAINDER; gbc.fill = + * GridBagConstraints.HORIZONTAL; gbc.anchor = + * GridBagConstraints.NORTHWEST; gbc.weightx = 1.0; gbc.weighty = 1.0; + * gbc.insets = new Insets(20, 30, 20, 30); + * gridbag.setConstraints(connStatusLabel, gbc); + * vncContainer.add(connStatusLabel); } + * + * if (inSeparateFrame) { vncFrame.pack(); } else { validate(); } + */ + } + + // + // Show an authentication panel. + // +/* + String askPassword() throws Exception { + showConnectionStatus(null); + + AuthPanel authPanel = new AuthPanel(this); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.ipadx = 100; + gbc.ipady = 50; + gridbag.setConstraints(authPanel, gbc); + vncContainer.add(authPanel); + + if (inSeparateFrame) { + vncFrame.pack(); + } else { + applet.validate(); + } + + authPanel.moveFocusToDefaultField(); + String pw = authPanel.getPassword(); + vncContainer.remove(authPanel); + + return pw; + } +*/ + // + // Do the rest of the protocol initialisation. + // + + void doProtocolInitialisation() throws IOException { + rfb.writeClientInit(); + rfb.readServerInit(); + /* + * Logger.log(Logger.INFO, "Desktop name is " + rfb.desktopName); + * Logger.log(Logger.INFO, "Desktop size is " + rfb.framebufferWidth + + * " x " + rfb.framebufferHeight); + */ + setEncodings(); + + showConnectionStatus(null); + } + + // + // Send current encoding list to the RFB server. + // + + int[] encodingsSaved; + int nEncodingsSaved; + + void setEncodings() { + setEncodings(false); + } + + void autoSelectEncodings() { + setEncodings(true); + } + + void setEncodings(boolean autoSelectOnly) { + if (options == null || rfb == null || !rfb.inNormalProtocol) + return; + + int preferredEncoding = options.preferredEncoding; + if (preferredEncoding == -1) { + long kbitsPerSecond = rfb.kbitsPerSecond(); + if (nEncodingsSaved < 1) { + // Choose Tight or ZRLE encoding for the very first update. + // Logger.log(Logger.INFO, "Using Tight/ZRLE encodings"); + preferredEncoding = RfbProto.EncodingTight; + } else if (kbitsPerSecond > 2000 + && encodingsSaved[0] != RfbProto.EncodingHextile) { + // Switch to Hextile if the connection speed is above 2Mbps. + s_logger.info("Throughput " + kbitsPerSecond + + " kbit/s - changing to Hextile encoding"); + preferredEncoding = RfbProto.EncodingHextile; + } else if (kbitsPerSecond < 1000 + && encodingsSaved[0] != RfbProto.EncodingTight) { + // Switch to Tight/ZRLE if the connection speed is below 1Mbps. + s_logger.info("Throughput " + kbitsPerSecond + + " kbit/s - changing to Tight/ZRLE encodings"); + preferredEncoding = RfbProto.EncodingTight; + } else { + // Don't change the encoder. + if (autoSelectOnly) + return; + preferredEncoding = encodingsSaved[0]; + } + } else { + // Auto encoder selection is not enabled. + if (autoSelectOnly) + return; + } + + int[] encodings = new int[20]; + int nEncodings = 0; + + encodings[nEncodings++] = preferredEncoding; + if (options.useCopyRect) { + encodings[nEncodings++] = RfbProto.EncodingCopyRect; + } + + if (preferredEncoding != RfbProto.EncodingTight) { + encodings[nEncodings++] = RfbProto.EncodingTight; + } + if (preferredEncoding != RfbProto.EncodingZRLE) { + encodings[nEncodings++] = RfbProto.EncodingZRLE; + } + if (preferredEncoding != RfbProto.EncodingHextile) { + encodings[nEncodings++] = RfbProto.EncodingHextile; + } + if (preferredEncoding != RfbProto.EncodingZlib) { + encodings[nEncodings++] = RfbProto.EncodingZlib; + } + if (preferredEncoding != RfbProto.EncodingCoRRE) { + encodings[nEncodings++] = RfbProto.EncodingCoRRE; + } + if (preferredEncoding != RfbProto.EncodingRRE) { + encodings[nEncodings++] = RfbProto.EncodingRRE; + } + + if (options.compressLevel >= 0 && options.compressLevel <= 9) { + encodings[nEncodings++] = RfbProto.EncodingCompressLevel0 + + options.compressLevel; + } + if (options.jpegQuality >= 0 && options.jpegQuality <= 9) { + encodings[nEncodings++] = RfbProto.EncodingQualityLevel0 + + options.jpegQuality; + } + + if (options.requestCursorUpdates) { + encodings[nEncodings++] = RfbProto.EncodingXCursor; + encodings[nEncodings++] = RfbProto.EncodingRichCursor; + if (!options.ignoreCursorUpdates) + encodings[nEncodings++] = RfbProto.EncodingPointerPos; + } + + encodings[nEncodings++] = RfbProto.EncodingLastRect; + encodings[nEncodings++] = RfbProto.EncodingNewFBSize; + + boolean encodingsWereChanged = false; + if (nEncodings != nEncodingsSaved) { + encodingsWereChanged = true; + } else { + for (int i = 0; i < nEncodings; i++) { + if (encodings[i] != encodingsSaved[i]) { + encodingsWereChanged = true; + break; + } + } + } + + if (encodingsWereChanged) { + try { + rfb.writeSetEncodings(encodings, nEncodings); + if (vc != null) { + vc.softCursorFree(); + } + } catch (Exception e) { + s_logger.error(e.toString(), e); + } + encodingsSaved = encodings; + nEncodingsSaved = nEncodings; + } + } + + // + // setCutText() - send the given cut text to the RFB server. + // + + void setCutText(String text) { + try { + if (rfb != null && rfb.inNormalProtocol) { + rfb.writeClientCutText(text); + } + } catch (Exception e) { + s_logger.error(e.toString(), e); + } + } + + // + // Order change in session recording status. To stop recording, pass + // null in place of the fname argument. + // + + void setRecordingStatus(String fname) { + synchronized (recordingSync) { + sessionFileName = fname; + recordingStatusChanged = true; + } + } + + // + // Start or stop session recording. Returns true if this method call + // causes recording of a new session. + // + + boolean checkRecordingStatus() throws IOException { + synchronized (recordingSync) { + if (recordingStatusChanged) { + recordingStatusChanged = false; + if (sessionFileName != null) { + startRecording(); + return true; + } else { + stopRecording(); + } + } + } + return false; + } + + // + // Start session recording. + // + + protected void startRecording() throws IOException { + /* + * synchronized(recordingSync) { + * + * if (!recordingActive) { // Save settings to restore them after + * recording the session. cursorUpdatesDef = + * options.choices[options.cursorUpdatesIndex].getSelectedItem(); + * eightBitColorsDef = + * options.choices[options.eightBitColorsIndex].getSelectedItem(); // + * Set options to values suitable for recording. + * options.choices[options.cursorUpdatesIndex].select("Disable"); + * options.choices[options.cursorUpdatesIndex].setEnabled(false); + * options.setEncodings(); + * options.choices[options.eightBitColorsIndex].select("No"); + * options.choices[options.eightBitColorsIndex].setEnabled(false); + * options.setColorFormat(); } else { rfb.closeSession(); } + * + * Logger.log(Logger.INFO, "Recording the session in " + + * sessionFileName); rfb.startSession(sessionFileName); recordingActive + * = true; } + */ + } + + // + // Stop session recording. + // + + protected void stopRecording() throws IOException { + /* + * synchronized(recordingSync) { if (recordingActive) { // Restore + * options. + * options.choices[options.cursorUpdatesIndex].select(cursorUpdatesDef); + * options.choices[options.cursorUpdatesIndex].setEnabled(true); + * options.setEncodings(); + * options.choices[options.eightBitColorsIndex].select + * (eightBitColorsDef); + * options.choices[options.eightBitColorsIndex].setEnabled(true); + * options.setColorFormat(); + * + * rfb.closeSession(); Logger.log(Logger.INFO, + * "Session recording stopped."); } sessionFileName = null; + * recordingActive = false; } + */ + } + + // + // readParameters() - read parameters from the html source or from the + // command line. On the command line, the arguments are just a sequence of + // param_name/param_value pairs where the names and values correspond to + // those expected in the html applet tag source. + // + + void readParameters() { + String str; + host = readParameter("HOST", true); + + str = readParameter("PORT", true); + port = Integer.parseInt(str); + + proxyHost = readParameter("PROXYHOST", false); + if (proxyHost != null) { + str = readParameter("PROXYPORT", true); + proxyPort = Integer.parseInt(str); + } + + // Read "ENCPASSWORD" or "PASSWORD" parameter if specified. + readPasswordParameters(); + + if (inAnApplet) { + str = readParameter("Open New Window", false); + if (str != null && str.equalsIgnoreCase("Yes")) + inSeparateFrame = true; + } + + // "Show Controls" set to "No" disables button panel. + showControls = false; + str = readParameter("Show Controls", false); + if (str != null && str.equalsIgnoreCase("No")) + showControls = false; + + // "Offer Relogin" set to "No" disables "Login again" and "Close + // window" buttons under error messages in applet mode. + offerRelogin = false; + str = readParameter("Offer Relogin", false); + if (str != null && str.equalsIgnoreCase("No")) + offerRelogin = false; + + // Do we continue showing desktop on remote disconnect? + showOfflineDesktop = false; + str = readParameter("Show Offline Desktop", false); + if (str != null && str.equalsIgnoreCase("Yes")) + showOfflineDesktop = true; + + // Fine tuning options. + deferScreenUpdates = readIntParameter("Defer screen updates", 20); + deferCursorUpdates = readIntParameter("Defer cursor updates", 10); + deferUpdateRequests = readIntParameter("Defer update requests", 50); + + // SocketFactory. + socketFactory = readParameter("SocketFactory", false); + } + + // + // Read password parameters. + // + + private void readPasswordParameters() { + String str = readParameter("SID", false); + if (str != null) + passwordParam = str; + } + + public String readParameter(String name, boolean required) { + if (inAnApplet) { + String s = applet.getParameter(name); + // s_logger.info("getParameter " + name + " = " + s); + if ((s == null) && required) { + fatalError(name + " parameter not specified"); + } + return s; + } + + for (int i = 0; i < mainArgs.length; i += 2) { + if (mainArgs[i].equalsIgnoreCase(name)) { + try { + return mainArgs[i + 1]; + } catch (Exception e) { + if (required) { + fatalError(name + " parameter not specified"); + } + return null; + } + } + } + if (required) { + fatalError(name + " parameter not specified"); + } + return null; + } + + int readIntParameter(String name, int defaultValue) { + String str = readParameter(name, false); + int result = defaultValue; + if (str != null) { + try { + result = Integer.parseInt(str); + } catch (NumberFormatException e) { + } + } + return result; + } + + // + // moveFocusToDesktop() - move keyboard focus either to VncCanvas. + // + + void moveFocusToDesktop() { + if (vncContainer != null) { + if (vc != null && vncContainer.isAncestorOf(vc)) + vc.requestFocus(); + } + } + + // + // disconnect() - close connection to server. + // + + synchronized public void disconnect() { + s_logger.info("Disconnect"); + + if (rfb != null) + rfb.close(); + + // options.dispose(); + // clipboard.dispose(); + if (rec != null) + rec.dispose(); + + if (inAnApplet) { + showMessage("Disconnected"); + } else { + System.exit(0); + } + } + + // + // fatalError() - print out a fatal error message. + // FIXME: Do we really need two versions of the fatalError() method? + // + + synchronized public void fatalError(String str) { + s_logger.info(str); + + if (inAnApplet) { + // vncContainer null, applet not inited, + // can not present the error to the user. + Thread.currentThread().stop(); + } else { + System.exit(1); + } + } + + synchronized public void fatalError(String str, Exception e) { + /* + * if (rfb != null && rfb.closed()) { // Not necessary to show error + * message if the error was caused // by I/O problems after the + * rfb.close() method call. Logger.log(Logger.INFO, + * "RFB thread finished"); return; } + */ + if (str == null) { + str = ""; + } + s_logger.info(str, e); + showMessage(str); + } + + // + // Show message text and optionally "Relogin" and "Close" buttons. + // + + void showMessage(String msg) { + if (vc != null && vc.memGraphics != null) { + vc.paintErrorString(msg); + return; + } else { + if(applet != null) + applet.paintErrorString(msg); + } + + s_logger.info(msg); + + /* + * vncContainer.removeAll(); + * + * Logger.log(Logger.INFO, "showMessage " + msg); + * + * Label errLabel = new Label(msg, Label.CENTER); errLabel.setFont(new + * Font("Helvetica", Font.PLAIN, 12)); + * + * if (offerRelogin) { + * + * Panel gridPanel = new Panel(new GridLayout(0, 1)); Panel outerPanel = + * new Panel(new FlowLayout(FlowLayout.LEFT)); + * outerPanel.add(gridPanel); vncContainer.setLayout(new + * FlowLayout(FlowLayout.LEFT, 30, 16)); vncContainer.add(outerPanel); + * Panel textPanel = new Panel(new FlowLayout(FlowLayout.CENTER)); + * textPanel.add(errLabel); gridPanel.add(textPanel); gridPanel.add(new + * ReloginPanel(this)); + * + * } else { + * + * vncContainer.setLayout(new FlowLayout(FlowLayout.LEFT, 30, 30)); + * vncContainer.add(errLabel); + * + * } + * + * if (inSeparateFrame) { vncFrame.pack(); } else { validate(); } + */ + } + + // + // Stop the applet. + // Main applet thread will terminate on first exception + // after seeing that rfb has been closed. + // + + public void stop() { + s_logger.info("Stopping applet"); + synchronized (this) { + if (rfb != null) { + rfb.close(); + } + } + + s_logger.info("Join RFB thread"); + try { + rfbThread.join(1000); + } catch (InterruptedException e) { + } + s_logger.info("Applet stopped"); + } + + // + // This method is called before the applet is destroyed. + // + + public void destroy() { + s_logger.info("Destroying applet"); + + vncContainer.removeAll(); + // options.dispose(); + // clipboard.dispose(); + if (rec != null) + rec.dispose(); + synchronized (this) { + if (rfb != null) + rfb.close(); + } + if (inSeparateFrame) + vncFrame.dispose(); + } + + // + // Start/stop receiving mouse events. + // + + public void enableInput(boolean enable) { + vc.enableInput(enable); + } + + // + // Close application properly on window close event. + // + + public void windowClosing(WindowEvent evt) { + s_logger.info("Closing window"); + if (rfb != null) + disconnect(); + + vncContainer.hide(); + + if (!inAnApplet) { + System.exit(0); + } + } + + public String toString() { + return ("{ConsoleViewer-" + + host + + ":" + + port + + (" created=\"" + new Date(createTime)) + "\"" + + (" lastused=\"" + new Date(lastUsedTime)) + "\"" + + (clientStream != null ? (" client=" + clientStreamInfo) : "") + "}"); + } + + // + // Implement RfbViewer interface + // + public boolean isProxy() { + return false; + } + + public boolean hasClientConnection() { + return false; + } + + public RfbProto getRfb() { + return rfb; + } + + public Dimension getScreenSize() { + return vncFrame.getToolkit().getScreenSize(); + } + + public Dimension getFrameSize() { + return vncFrame.getSize(); + } + + public int getScalingFactor() { + return options.scalingFactor; + } + + public int getCursorScaleFactor() { + return options.scaleCursor; + } + + public boolean ignoreCursorUpdate() { + return options.ignoreCursorUpdates; + } + + public int getDeferCursorUpdateTimeout() { + return deferCursorUpdates; + } + + public int getDeferScreenUpdateTimeout() { + return deferScreenUpdates; + } + + public int getDeferUpdateRequestTimeout() { + return deferUpdateRequests; + } + + public int setPixelFormat(RfbProto rfb) throws IOException { + if (options.eightBitColors) { + rfb.writeSetPixelFormat(8, 8, false, true, 7, 7, 3, 0, 3, 6); + return 1; + } else { + rfb.writeSetPixelFormat(32, 24, false, true, 255, 255, 255, 16, 8, 0); + return 4; + } + } + + public void onCanvasInit() { + if (!options.viewOnly) + vc.enableInput(true); + } + + public void onInputEnabled(boolean enable) { + if(enable) { + if (showControls) { + buttonPanel.enableRemoteAccessControls(true); + } + } else { + if (showControls) { + buttonPanel.enableRemoteAccessControls(false); + } + } + } + + public void onFramebufferSizeChange(int w, int h) { + // Update the size of desktop containers. + if (inSeparateFrame) { + if (desktopScrollPane != null) + vc.resizeDesktopFrame(); + } else { + vc.setSize(vc.scaledWidth, vc.scaledHeight); + } + moveFocusToDesktop(); + + try { + rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth, + rfb.framebufferHeight, true); + } catch (IOException e) { + s_logger.warn("Exception when sending frame buffer update request", e); + } + } + + public void onFramebufferUpdate(int x, int y, int w, int h) { + } + + public void onFramebufferCursorMove(int x, int y) { + } + + public void onFramebufferCursorShapeChange(int encodingType, + int xhot, int yhot, int width, int height, byte[] cursorData) { + } + + public void onDesktopResize() { + Insets insets = desktopScrollPane.getInsets(); + desktopScrollPane.setSize(vc.scaledWidth + + 2 * Math.min(insets.left, insets.right), + vc.scaledHeight + + 2 * Math.min(insets.top, insets.bottom)); + + vncFrame.pack(); + } + + public void onFrameResize(Dimension newSize) { + vncFrame.setSize(newSize); + desktopScrollPane.doLayout(); + } + + public void onDisconnectMessage() { + // do nothing in viewer mode + } + + public void onBellMessage() { + Toolkit.getDefaultToolkit().beep(); + } + + public void onPreProtocolProcess(byte[] bs) throws IOException { + // do nothing in viwer mode + } + + public boolean onPostFrameBufferUpdateProcess(boolean cursorPosReceived) throws IOException { + boolean fullUpdateNeeded = false; + if (checkRecordingStatus()) + fullUpdateNeeded = true; + + // Defer framebuffer update request if necessary. But wake up + // immediately on keyboard or mouse event. Also, don't sleep + // if there is some data to receive, or if the last update + // included a PointerPos message. + if (deferUpdateRequests > 0 && rfb.is.available() == 0 && !cursorPosReceived) { + synchronized(vc.rfb) { + try { + vc.rfb.wait(deferUpdateRequests); + } catch (InterruptedException e) { + } + } + } + + // Before requesting framebuffer update, check if the pixel + // format should be changed. If it should, request full update + // instead of an incremental one. + if (options.eightBitColors != (vc.bytesPixel == 1)) { + vc.setPixelFormat(); + fullUpdateNeeded = true; + } + + return fullUpdateNeeded; + } + + public void onProtocolProcessException(IOException e) { + // do nothing in viewer mode + } + + public Socket createConnection(String host, int port) throws IOException { + + SocketFactory socketFactory = SSLSocketFactory.getDefault(); + Socket sock = socketFactory.createSocket(host, port); + sock.setSoTimeout(DEFAULT_READ_TIMEOUT_SECONDS*1000); + return sock; + } + + public void writeInit(OutputStream os) throws IOException { + if (options.shareDesktop) { + os.write(1); + } else { + os.write(0); + } + } + + public void swapMouseButton(Integer[] masks) { + if (options.reverseMouseButtons2And3) { + Integer temp = masks[1]; + masks[1] = masks[0]; + masks[0] = temp; + } + } +} diff --git a/console-viewer/src/com/cloud/consoleviewer/OptionsFrame.java b/console-viewer/src/com/cloud/consoleviewer/OptionsFrame.java new file mode 100644 index 00000000000..89d0590aa19 --- /dev/null +++ b/console-viewer/src/com/cloud/consoleviewer/OptionsFrame.java @@ -0,0 +1,418 @@ +// +// Copyright (C) 2001 HorizonLive.com, Inc. All Rights Reserved. +// Copyright (C) 2001 Constantin Kaplinsky. All Rights Reserved. +// Copyright (C) 2000 Tridia Corporation. All Rights Reserved. +// Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// +// Options frame. +// +// This deals with all the options the user can play with. +// It sets the encodings array and some booleans. +// + +package com.cloud.consoleviewer; + +import java.awt.*; +import java.awt.event.*; + +class OptionsFrame { +/* extends Frame +} + implements WindowListener, ActionListener, ItemListener { + + static String[] names = { + "Encoding", + "Compression level", + "JPEG image quality", + "Cursor shape updates", + "Use CopyRect", + "Restricted colors", + "Mouse buttons 2 and 3", + "View only", + "Scale remote cursor", + "Share desktop", + }; + + static String[][] values = { + { "Auto", "Raw", "RRE", "CoRRE", "Hextile", "Zlib", "Tight", "ZRLE" }, + { "Default", "1", "2", "3", "4", "5", "6", "7", "8", "9" }, + { "JPEG off", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }, + { "Enable", "Ignore", "Disable" }, + { "Yes", "No" }, + { "Yes", "No" }, + { "Normal", "Reversed" }, + { "Yes", "No" }, + { "No", "50%", "75%", "125%", "150%" }, + { "Yes", "No" }, + }; + + final int + encodingIndex = 0, + compressLevelIndex = 1, + jpegQualityIndex = 2, + cursorUpdatesIndex = 3, + useCopyRectIndex = 4, + eightBitColorsIndex = 5, + mouseButtonIndex = 6, + viewOnlyIndex = 7, + scaleCursorIndex = 8, + shareDesktopIndex = 9; + + Label[] labels = new Label[names.length]; + Choice[] choices = new Choice[names.length]; + Button closeButton; + VncViewer viewer; + +*/ + // + // The actual data which other classes look at: + // + + int preferredEncoding = -1; + int compressLevel = 5; + int jpegQuality = 5; + boolean useCopyRect = false; + boolean requestCursorUpdates = true; + boolean ignoreCursorUpdates = false; + + boolean eightBitColors = false; + + boolean reverseMouseButtons2And3 = false; + boolean shareDesktop = true; + boolean viewOnly = false; + int scaleCursor = 0; + + boolean autoScale = false; + int scalingFactor = 100; + + // + // Constructor. Set up the labels and choices from the names and values + // arrays. + // + + OptionsFrame(ConsoleViewer v) { + } + /* + super("TightVNC Options"); + + viewer = v; + + GridBagLayout gridbag = new GridBagLayout(); + setLayout(gridbag); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + + for (int i = 0; i < names.length; i++) { + labels[i] = new Label(names[i]); + gbc.gridwidth = 1; + gridbag.setConstraints(labels[i],gbc); + add(labels[i]); + + choices[i] = new Choice(); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gridbag.setConstraints(choices[i],gbc); + add(choices[i]); + choices[i].addItemListener(this); + + for (int j = 0; j < values[i].length; j++) { + choices[i].addItem(values[i][j]); + } + } + + closeButton = new Button("Close"); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gridbag.setConstraints(closeButton, gbc); + add(closeButton); + closeButton.addActionListener(this); + + pack(); + + addWindowListener(this); + + // Set up defaults + + choices[encodingIndex].select("Auto"); + choices[compressLevelIndex].select("Default"); + choices[jpegQualityIndex].select("6"); + choices[cursorUpdatesIndex].select("Enable"); + choices[useCopyRectIndex].select("Yes"); + choices[eightBitColorsIndex].select("No"); + choices[mouseButtonIndex].select("Normal"); + choices[viewOnlyIndex].select("No"); + choices[scaleCursorIndex].select("No"); + choices[shareDesktopIndex].select("Yes"); + + // But let them be overridden by parameters + + for (int i = 0; i < names.length; i++) { + String s = viewer.readParameter(names[i], false); + if (s != null) { + for (int j = 0; j < values[i].length; j++) { + if (s.equalsIgnoreCase(values[i][j])) { + choices[i].select(j); + } + } + } + } + + // FIXME: Provide some sort of GUI for "Scaling Factor". + + autoScale = false; + scalingFactor = 100; + String s = viewer.readParameter("Scaling Factor", false); + if (s != null) { + if (s.equalsIgnoreCase("Auto")) { + autoScale = true; + } else { + // Remove the '%' char at the end of string if present. + if (s.charAt(s.length() - 1) == '%') { + s = s.substring(0, s.length() - 1); + } + // Convert to an integer. + try { + scalingFactor = Integer.parseInt(s); + } + catch (NumberFormatException e) { + scalingFactor = 100; + } + // Make sure scalingFactor is in the range of [1..1000]. + if (scalingFactor < 1) { + scalingFactor = 1; + } else if (scalingFactor > 1000) { + scalingFactor = 1000; + } + } + } + + // Make the booleans and encodings array correspond to the state of the GUI + + setEncodings(); + setColorFormat(); + setOtherOptions(); + } + + + // + // Disable the shareDesktop option + // + + void disableShareDesktop() { + labels[shareDesktopIndex].setEnabled(false); + choices[shareDesktopIndex].setEnabled(false); + } + + + // + // setEncodings looks at the encoding, compression level, JPEG + // quality level, cursor shape updates and copyRect choices and sets + // corresponding variables properly. Then it calls the VncViewer's + // setEncodings method to send a SetEncodings message to the RFB + // server. + // + + void setEncodings() { + useCopyRect = choices[useCopyRectIndex].getSelectedItem().equals("Yes"); + + preferredEncoding = RfbProto.EncodingRaw; + boolean enableCompressLevel = false; + boolean enableQualityLevel = false; + + if (choices[encodingIndex].getSelectedItem().equals("RRE")) { + preferredEncoding = RfbProto.EncodingRRE; + } else if (choices[encodingIndex].getSelectedItem().equals("CoRRE")) { + preferredEncoding = RfbProto.EncodingCoRRE; + } else if (choices[encodingIndex].getSelectedItem().equals("Hextile")) { + preferredEncoding = RfbProto.EncodingHextile; + } else if (choices[encodingIndex].getSelectedItem().equals("ZRLE")) { + preferredEncoding = RfbProto.EncodingZRLE; + } else if (choices[encodingIndex].getSelectedItem().equals("Zlib")) { + preferredEncoding = RfbProto.EncodingZlib; + enableCompressLevel = true; + } else if (choices[encodingIndex].getSelectedItem().equals("Tight")) { + preferredEncoding = RfbProto.EncodingTight; + enableCompressLevel = true; + enableQualityLevel = !eightBitColors; + } else if (choices[encodingIndex].getSelectedItem().equals("Auto")) { + preferredEncoding = -1; + enableQualityLevel = !eightBitColors; + } + + // Handle compression level setting. + + try { + compressLevel = + Integer.parseInt(choices[compressLevelIndex].getSelectedItem()); + } + catch (NumberFormatException e) { + compressLevel = -1; + } + if (compressLevel < 1 || compressLevel > 9) { + compressLevel = -1; + } + labels[compressLevelIndex].setEnabled(enableCompressLevel); + choices[compressLevelIndex].setEnabled(enableCompressLevel); + + // Handle JPEG quality setting. + + try { + jpegQuality = + Integer.parseInt(choices[jpegQualityIndex].getSelectedItem()); + } + catch (NumberFormatException e) { + jpegQuality = -1; + } + if (jpegQuality < 0 || jpegQuality > 9) { + jpegQuality = -1; + } + labels[jpegQualityIndex].setEnabled(enableQualityLevel); + choices[jpegQualityIndex].setEnabled(enableQualityLevel); + + // Request cursor shape updates if necessary. + + requestCursorUpdates = + !choices[cursorUpdatesIndex].getSelectedItem().equals("Disable"); + + if (requestCursorUpdates) { + ignoreCursorUpdates = + choices[cursorUpdatesIndex].getSelectedItem().equals("Ignore"); + } + + viewer.setEncodings(); + } + + // + // setColorFormat sets eightBitColors variable depending on the GUI + // setting, causing switches between 8-bit and 24-bit colors mode if + // necessary. + // + + void setColorFormat() { + + eightBitColors = + choices[eightBitColorsIndex].getSelectedItem().equals("Yes"); + + boolean enableJPEG = !eightBitColors && + (choices[encodingIndex].getSelectedItem().equals("Tight") || + choices[encodingIndex].getSelectedItem().equals("Auto")); + + labels[jpegQualityIndex].setEnabled(enableJPEG); + choices[jpegQualityIndex].setEnabled(enableJPEG); + } + + // + // setOtherOptions looks at the "other" choices (ones which don't set the + // encoding or the color format) and sets the boolean flags appropriately. + // + + void setOtherOptions() { + + reverseMouseButtons2And3 + = choices[mouseButtonIndex].getSelectedItem().equals("Reversed"); + + viewOnly + = choices[viewOnlyIndex].getSelectedItem().equals("Yes"); + if (viewer.vc != null) + viewer.vc.enableInput(!viewOnly); + + shareDesktop + = choices[shareDesktopIndex].getSelectedItem().equals("Yes"); + + String scaleString = choices[scaleCursorIndex].getSelectedItem(); + if (scaleString.endsWith("%")) + scaleString = scaleString.substring(0, scaleString.length() - 1); + try { + scaleCursor = Integer.parseInt(scaleString); + } + catch (NumberFormatException e) { + scaleCursor = 0; + } + if (scaleCursor < 10 || scaleCursor > 500) { + scaleCursor = 0; + } + if (requestCursorUpdates && !ignoreCursorUpdates && !viewOnly) { + labels[scaleCursorIndex].setEnabled(true); + choices[scaleCursorIndex].setEnabled(true); + } else { + labels[scaleCursorIndex].setEnabled(false); + choices[scaleCursorIndex].setEnabled(false); + } + if (viewer.vc != null) + viewer.vc.createSoftCursor(); // update cursor scaling + } + + + // + // Respond to actions on Choice controls + // + + public void itemStateChanged(ItemEvent evt) { + Object source = evt.getSource(); + + if (source == choices[encodingIndex] || + source == choices[compressLevelIndex] || + source == choices[jpegQualityIndex] || + source == choices[cursorUpdatesIndex] || + source == choices[useCopyRectIndex]) { + + setEncodings(); + + if (source == choices[cursorUpdatesIndex]) { + setOtherOptions(); // update scaleCursor state + } + + } else if (source == choices[eightBitColorsIndex]) { + + setColorFormat(); + + } else if (source == choices[mouseButtonIndex] || + source == choices[shareDesktopIndex] || + source == choices[viewOnlyIndex] || + source == choices[scaleCursorIndex]) { + + setOtherOptions(); + } + } + + // + // Respond to button press + // + + public void actionPerformed(ActionEvent evt) { + if (evt.getSource() == closeButton) + setVisible(false); + } + + // + // Respond to window events + // + + public void windowClosing(WindowEvent evt) { + setVisible(false); + } + + public void windowActivated(WindowEvent evt) {} + public void windowDeactivated(WindowEvent evt) {} + public void windowOpened(WindowEvent evt) {} + public void windowClosed(WindowEvent evt) {} + public void windowIconified(WindowEvent evt) {} + public void windowDeiconified(WindowEvent evt) {} +*/ + } diff --git a/console-viewer/src/com/cloud/consoleviewer/RecordingFrame.java b/console-viewer/src/com/cloud/consoleviewer/RecordingFrame.java new file mode 100644 index 00000000000..4b5effb63ae --- /dev/null +++ b/console-viewer/src/com/cloud/consoleviewer/RecordingFrame.java @@ -0,0 +1,314 @@ +// +// Copyright (C) 2002 Constantin Kaplinsky. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// +// Recording frame. It allows to control recording RFB sessions into +// FBS (FrameBuffer Stream) files. +// + +package com.cloud.consoleviewer; + +import java.io.*; +import java.awt.*; +import java.awt.event.*; + +@SuppressWarnings("serial") +class RecordingFrame extends Frame + implements WindowListener, ActionListener { + + boolean recording; + + TextField fnameField; + Button browseButton; + + Label statusLabel; + + Button recordButton, nextButton, closeButton; + ConsoleViewer viewer; + + // + // Check if current security manager allows to create a + // RecordingFrame object. + // + + public static boolean checkSecurity() { + SecurityManager security = System.getSecurityManager(); + if (security != null) { + try { + security.checkPropertyAccess("user.dir"); + security.checkPropertyAccess("file.separator"); + // Work around (rare) checkPropertyAccess bug + System.getProperty("user.dir"); + } catch (SecurityException e) { + System.out.println("SecurityManager restricts session recording."); + return false; + } + } + return true; + } + + // + // Constructor. + // + + RecordingFrame(ConsoleViewer v) { + super("TightVNC Session Recording"); + + viewer = v; + + // Determine initial filename for next saved session. + // FIXME: Check SecurityManager. + + String fname = nextNewFilename(System.getProperty("user.dir") + + System.getProperty("file.separator") + + "vncsession.fbs"); + + // Construct new panel with file name field and "Browse" button. + + Panel fnamePanel = new Panel(); + GridBagLayout fnameGridbag = new GridBagLayout(); + fnamePanel.setLayout(fnameGridbag); + + GridBagConstraints fnameConstraints = new GridBagConstraints(); + fnameConstraints.gridwidth = GridBagConstraints.RELATIVE; + fnameConstraints.fill = GridBagConstraints.BOTH; + fnameConstraints.weightx = 4.0; + + fnameField = new TextField(fname, 64); + fnameGridbag.setConstraints(fnameField, fnameConstraints); + fnamePanel.add(fnameField); + fnameField.addActionListener(this); + + fnameConstraints.gridwidth = GridBagConstraints.REMAINDER; + fnameConstraints.weightx = 1.0; + + browseButton = new Button("Browse"); + fnameGridbag.setConstraints(browseButton, fnameConstraints); + fnamePanel.add(browseButton); + browseButton.addActionListener(this); + + // Construct the frame. + + GridBagLayout gridbag = new GridBagLayout(); + setLayout(gridbag); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.fill = GridBagConstraints.BOTH; + gbc.weighty = 1.0; + gbc.insets = new Insets(10, 0, 0, 0); + + Label helpLabel = + new Label("File name to save next recorded session in:", Label.CENTER); + gridbag.setConstraints(helpLabel, gbc); + add(helpLabel); + + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weighty = 0.0; + gbc.insets = new Insets(0, 0, 0, 0); + + gridbag.setConstraints(fnamePanel, gbc); + add(fnamePanel); + + gbc.fill = GridBagConstraints.BOTH; + gbc.weighty = 1.0; + gbc.insets = new Insets(10, 0, 10, 0); + + statusLabel = new Label("", Label.CENTER); + gridbag.setConstraints(statusLabel, gbc); + add(statusLabel); + + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.weighty = 0.0; + gbc.gridwidth = 1; + gbc.insets = new Insets(0, 0, 0, 0); + + recordButton = new Button("Record"); + gridbag.setConstraints(recordButton, gbc); + add(recordButton); + recordButton.addActionListener(this); + + nextButton = new Button("Next file"); + gridbag.setConstraints(nextButton, gbc); + add(nextButton); + nextButton.addActionListener(this); + + closeButton = new Button("Close"); + gridbag.setConstraints(closeButton, gbc); + add(closeButton); + closeButton.addActionListener(this); + + // Set correct text, font and color for the statusLabel. + stopRecording(); + + pack(); + + addWindowListener(this); + } + + // + // If the given string ends with ".NNN" where NNN is a decimal + // number, increase this number by one. Otherwise, append ".001" + // to the given string. + // + + protected String nextFilename(String fname) { + int len = fname.length(); + int suffixPos = len; + int suffixNum = 1; + + if (len > 4 && fname.charAt(len - 4) == '.') { + try { + suffixNum = Integer.parseInt(fname.substring(len - 3, len)) + 1; + suffixPos = len - 4; + } catch (NumberFormatException e) { } + } + + char[] zeroes = {'0', '0', '0'}; + String suffix = String.valueOf(suffixNum); + if (suffix.length() < 3) { + suffix = new String(zeroes, 0, 3 - suffix.length()) + suffix; + } + + return fname.substring(0, suffixPos) + '.' + suffix; + } + + // + // Find next name of a file which does not exist yet. + // + + protected String nextNewFilename(String fname) { + String newName = fname; + File f; + try { + do { + newName = nextFilename(newName); + f = new File(newName); + } while (f.exists()); + } catch (SecurityException e) { } + + return newName; + } + + // + // Let the user choose a file name showing a FileDialog. + // + + protected boolean browseFile() { + File currentFile = new File(fnameField.getText()); + + FileDialog fd = + new FileDialog(this, "Save next session as...", FileDialog.SAVE); + fd.setDirectory(currentFile.getParent()); + fd.setVisible(true); + if (fd.getFile() != null) { + String newDir = fd.getDirectory(); + String sep = System.getProperty("file.separator"); + if (newDir.length() > 0) { + if (!sep.equals(newDir.substring(newDir.length() - sep.length()))) + newDir += sep; + } + String newFname = newDir + fd.getFile(); + if (newFname.equals(fnameField.getText())) { + fnameField.setText(newFname); + return true; + } + } + return false; + } + + // + // Start recording. + // + + public void startRecording() { + statusLabel.setText("Status: Recording..."); + statusLabel.setFont(new Font("Helvetica", Font.BOLD, 12)); + statusLabel.setForeground(Color.red); + recordButton.setLabel("Stop recording"); + + recording = true; + + viewer.setRecordingStatus(fnameField.getText()); + } + + // + // Stop recording. + // + + public void stopRecording() { + statusLabel.setText("Status: Not recording."); + statusLabel.setFont(new Font("Helvetica", Font.PLAIN, 12)); + statusLabel.setForeground(Color.black); + recordButton.setLabel("Record"); + + recording = false; + + viewer.setRecordingStatus(null); + } + + // + // Close our window properly. + // + + public void windowClosing(WindowEvent evt) { + setVisible(false); + } + + // + // Ignore window events we're not interested in. + // + + public void windowActivated(WindowEvent evt) {} + public void windowDeactivated (WindowEvent evt) {} + public void windowOpened(WindowEvent evt) {} + public void windowClosed(WindowEvent evt) {} + public void windowIconified(WindowEvent evt) {} + public void windowDeiconified(WindowEvent evt) {} + + + // + // Respond to button presses + // + + public void actionPerformed(ActionEvent evt) { + if (evt.getSource() == browseButton) { + if (browseFile() && recording) + startRecording(); + + } else if (evt.getSource() == recordButton) { + if (!recording) { + startRecording(); + } else { + stopRecording(); + fnameField.setText(nextNewFilename(fnameField.getText())); + } + + } else if (evt.getSource() == nextButton) { + fnameField.setText(nextNewFilename(fnameField.getText())); + if (recording) + startRecording(); + + } else if (evt.getSource() == closeButton) { + setVisible(false); + + } + } +} diff --git a/console-viewer/src/com/cloud/consoleviewer/ReloginPanel.java b/console-viewer/src/com/cloud/consoleviewer/ReloginPanel.java new file mode 100644 index 00000000000..7c1ba17b56f --- /dev/null +++ b/console-viewer/src/com/cloud/consoleviewer/ReloginPanel.java @@ -0,0 +1,66 @@ +// +// Copyright (C) 2002 Cendio Systems. All Rights Reserved. +// Copyright (C) 2002 Constantin Kaplinsky. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// +// ReloginPanel class implements panel with a button for logging in again, +// after fatal errors or disconnect +// + +package com.cloud.consoleviewer; + +import java.awt.*; +import java.awt.event.*; +import java.applet.*; + +// +// The panel which implements the Relogin button +// +@SuppressWarnings("serial") +class ReloginPanel extends Panel implements ActionListener { + Button reloginButton; + Button closeButton; + ConsoleViewer viewer; + + // + // Constructor. + // + public ReloginPanel(ConsoleViewer v) { + viewer = v; + setLayout(new FlowLayout(FlowLayout.CENTER)); + reloginButton = new Button("Login again"); + add(reloginButton); + reloginButton.addActionListener(this); + if (viewer.inSeparateFrame) { + closeButton = new Button("Close window"); + add(closeButton); + closeButton.addActionListener(this); + } + } + + // + // This method is called when a button is pressed. + // + public synchronized void actionPerformed(ActionEvent evt) { + if (viewer.inSeparateFrame) + viewer.vncFrame.dispose(); + if (evt.getSource() == reloginButton) + viewer.applet.getAppletContext().showDocument(viewer.applet.getDocumentBase()); + } +} diff --git a/console-viewer/test/test-applet.html b/console-viewer/test/test-applet.html new file mode 100644 index 00000000000..b68cd7c94c5 --- /dev/null +++ b/console-viewer/test/test-applet.html @@ -0,0 +1,48 @@ + +Console Viewer Applet + + + +
+ +
+ + + + + + + + + + + + + + + diff --git a/console/.classpath b/console/.classpath new file mode 100644 index 00000000000..d171cd4c123 --- /dev/null +++ b/console/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/console/.project b/console/.project new file mode 100644 index 00000000000..b5e281cd87c --- /dev/null +++ b/console/.project @@ -0,0 +1,17 @@ + + + console-common + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/console/src/com/cloud/console/AuthenticationException.java b/console/src/com/cloud/console/AuthenticationException.java new file mode 100644 index 00000000000..d1f3bb2e74c --- /dev/null +++ b/console/src/com/cloud/console/AuthenticationException.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +public class AuthenticationException extends Exception { + private static final long serialVersionUID = -393139302884898842L; + public AuthenticationException() { + super(); + } + public AuthenticationException(String s) { + super(s); + } + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } + public AuthenticationException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/console/src/com/cloud/console/CapabilityInfo.java b/console/src/com/cloud/console/CapabilityInfo.java new file mode 100644 index 00000000000..3c8b8d505fb --- /dev/null +++ b/console/src/com/cloud/console/CapabilityInfo.java @@ -0,0 +1,90 @@ +// +// Copyright (C) 2003 Constantin Kaplinsky. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// +// CapabilityInfo.java - A class to hold information about a +// particular capability as used in the RFB protocol 3.130. +// + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +public class CapabilityInfo { + + // Public methods + + public CapabilityInfo(int code, + String vendorSignature, + String nameSignature, + String description) { + this.code = code; + this.vendorSignature = vendorSignature; + this.nameSignature = nameSignature; + this.description = description; + enabled = false; + } + + public CapabilityInfo(int code, + byte[] vendorSignature, + byte[] nameSignature) { + this.code = code; + this.vendorSignature = new String(vendorSignature); + this.nameSignature = new String(nameSignature); + this.description = null; + enabled = false; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + public boolean isEnabled() { + return enabled; + } + + public void enable() { + enabled = true; + } + + public boolean equals(CapabilityInfo other) { + return (other != null && this.code == other.code && + this.vendorSignature.equals(other.vendorSignature) && + this.nameSignature.equals(other.nameSignature)); + } + + public boolean enableIfEquals(CapabilityInfo other) { + if (this.equals(other)) + enable(); + + return isEnabled(); + } + + // Protected data + + protected int code; + protected String vendorSignature; + protected String nameSignature; + + protected String description; + protected boolean enabled; +} diff --git a/console/src/com/cloud/console/CapsContainer.java b/console/src/com/cloud/console/CapsContainer.java new file mode 100644 index 00000000000..82a85dedee1 --- /dev/null +++ b/console/src/com/cloud/console/CapsContainer.java @@ -0,0 +1,109 @@ +// +// Copyright (C) 2003 Constantin Kaplinsky. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// +// CapsContainer.java - A container of capabilities as used in the RFB +// protocol 3.130 +// + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +import java.util.Vector; +import java.util.Hashtable; + +public class CapsContainer { + + // Public methods + + public CapsContainer() { + infoMap = new Hashtable(64, (float)0.25); + orderedList = new Vector(32, 8); + } + + @SuppressWarnings("unchecked") + public void add(CapabilityInfo capinfo) { + Integer key = new Integer(capinfo.getCode()); + infoMap.put(key, capinfo); + } + + @SuppressWarnings("unchecked") + public void add(int code, String vendor, String name, String desc) { + Integer key = new Integer(code); + infoMap.put(key, new CapabilityInfo(code, vendor, name, desc)); + } + + public boolean isKnown(int code) { + return infoMap.containsKey(new Integer(code)); + } + + public CapabilityInfo getInfo(int code) { + return (CapabilityInfo)infoMap.get(new Integer(code)); + } + + public String getDescription(int code) { + CapabilityInfo capinfo = (CapabilityInfo)infoMap.get(new Integer(code)); + if (capinfo == null) + return null; + + return capinfo.getDescription(); + } + + @SuppressWarnings("unchecked") + public boolean enable(CapabilityInfo other) { + Integer key = new Integer(other.getCode()); + CapabilityInfo capinfo = (CapabilityInfo)infoMap.get(key); + if (capinfo == null) + return false; + + boolean enabled = capinfo.enableIfEquals(other); + if (enabled) + orderedList.addElement(key); + + return enabled; + } + + public boolean isEnabled(int code) { + CapabilityInfo capinfo = (CapabilityInfo)infoMap.get(new Integer(code)); + if (capinfo == null) + return false; + + return capinfo.isEnabled(); + } + + public int numEnabled() { + return orderedList.size(); + } + + public int getByOrder(int idx) { + int code; + try { + code = ((Integer)orderedList.elementAt(idx)).intValue(); + } catch (ArrayIndexOutOfBoundsException e) { + code = 0; + } + return code; + } + + // Protected data + + protected Hashtable infoMap; + protected Vector orderedList; +} + diff --git a/console/src/com/cloud/console/ConsoleCanvas.java b/console/src/com/cloud/console/ConsoleCanvas.java new file mode 100644 index 00000000000..fab81977131 --- /dev/null +++ b/console/src/com/cloud/console/ConsoleCanvas.java @@ -0,0 +1,2068 @@ +// +// Copyright (C) 2004 Horizon Wimba. All Rights Reserved. +// Copyright (C) 2001-2003 HorizonLive.com, Inc. All Rights Reserved. +// Copyright (C) 2001,2002 Constantin Kaplinsky. All Rights Reserved. +// Copyright (C) 2000 Tridia Corporation. All Rights Reserved. +// Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; +import java.io.*; +import java.util.zip.*; + +// +// VncCanvas is a subclass of Canvas which draws a VNC desktop on it. +// +@SuppressWarnings("serial") +public class ConsoleCanvas extends Canvas + implements KeyListener, MouseListener, MouseMotionListener { + + private static final Logger s_logger = Logger.getLogger(ConsoleCanvas.class); + + // ConsoleViewer viewer; + RfbViewer viewer; + public RfbProto rfb; + ColorModel cm8, cm24; + Color[] colors; + public int bytesPixel; + + int maxWidth = 0, maxHeight = 0; + int scalingFactor; + public int scaledWidth, scaledHeight; + + public Image memImage; + public Graphics memGraphics; + + Image rawPixelsImage; + MemoryImageSource pixelsSource; + byte[] pixels8; + int[] pixels24; + + // ZRLE encoder's data. + byte[] zrleBuf; + int zrleBufLen = 0; + byte[] zrleTilePixels8; + int[] zrleTilePixels24; + ZlibInStream zrleInStream; + boolean zrleRecWarningShown = false; + + // Zlib encoder's data. + byte[] zlibBuf; + int zlibBufLen = 0; + Inflater zlibInflater; + + // Tight encoder's data. + final static int tightZlibBufferSize = 512; + Inflater[] tightInflaters; + + // Since JPEG images are loaded asynchronously, we have to remember + // their position in the framebuffer. Also, this jpegRect object is + // used for synchronization between the rfbThread and a JVM's thread + // which decodes and loads JPEG images. + Rectangle jpegRect; + + // True if we process keyboard and mouse events. + boolean inputEnabled; + + // + // The constructors. + // + + // public ConsoleCanvas(ConsoleViewer v, int maxWidth_, int maxHeight_) { + public ConsoleCanvas(RfbViewer v, int maxWidth_, int maxHeight_) { + + viewer = v; + maxWidth = maxWidth_; + maxHeight = maxHeight_; + + rfb = viewer.getRfb(); + scalingFactor = viewer.getScalingFactor(); + + tightInflaters = new Inflater[4]; + + cm8 = new DirectColorModel(8, 7, (7 << 3), (3 << 6)); + cm24 = new DirectColorModel(24, 0xFF0000, 0x00FF00, 0x0000FF); + + colors = new Color[256]; + for (int i = 0; i < 256; i++) + colors[i] = new Color(cm8.getRGB(i)); + + inputEnabled = false; + + // Keyboard listener is enabled even in view-only mode, to catch + // 'r' or 'R' key presses used to request screen update. + addKeyListener(this); + } + + // public ConsoleCanvas(ConsoleViewer v) throws IOException { + public ConsoleCanvas(RfbViewer v) throws IOException { + this(v, 0, 0); + } + + // + // Callback methods to determine geometry of our Component. + // + + public Dimension getPreferredSize() { + return new Dimension(scaledWidth, scaledHeight); + } + + public Dimension getMinimumSize() { + return new Dimension(scaledWidth, scaledHeight); + } + + public Dimension getMaximumSize() { + return new Dimension(scaledWidth, scaledHeight); + } + + // + // All painting is performed here. + // + + public void update(Graphics g) { + paint(g); + } + + public void paint(Graphics g) { + // if(s_logger.isInfoEnabled()) + // s_logger.info("VncCanvas.paint called g=" + g.hashCode() + g + + // " memImage=" + memImage); + if (rfb == null) { + return; + } + + synchronized (memImage) { + if (rfb.framebufferWidth == scaledWidth) { + g.drawImage(memImage, 0, 0, null); + } else { + paintScaledFrameBuffer(g); + } + } + if (showSoftCursor) { + int x0 = cursorX - hotX, y0 = cursorY - hotY; + Rectangle r = new Rectangle(x0, y0, cursorWidth, cursorHeight); + if (r.intersects(g.getClipBounds())) { + g.drawImage(softCursor, x0, y0, null); + } + } + } + + public void paintScaledFrameBuffer(Graphics g) { + g.drawImage(memImage, 0, 0, scaledWidth, scaledHeight, null); + } + + // + // Override the ImageObserver interface method to handle drawing of + // JPEG-encoded data. + // + + public boolean imageUpdate(Image img, int infoflags, + int x, int y, int width, int height) { + if ((infoflags & (ALLBITS | ABORT)) == 0) { + return true; // We need more image data. + } else { + // If the whole image is available, draw it now. + if ((infoflags & ALLBITS) != 0) { + if (jpegRect != null) { + synchronized(jpegRect) { + memGraphics.drawImage(img, jpegRect.x, jpegRect.y, null); + scheduleRepaint(jpegRect.x, jpegRect.y, + jpegRect.width, jpegRect.height); + jpegRect.notify(); + } + } + } + return false; // All image data was processed. + } + } + + // + // Start/stop receiving mouse events. Keyboard events are received + // even in view-only mode, because we want to map the 'r' key to the + // screen refreshing function. + // + + public synchronized void enableInput(boolean enable) { + if (enable && !inputEnabled) { + inputEnabled = true; + addMouseListener(this); + addMouseMotionListener(this); + + viewer.onInputEnabled(true); + + createSoftCursor(); // scaled cursor + } else if (!enable && inputEnabled) { + inputEnabled = false; + removeMouseListener(this); + removeMouseMotionListener(this); + + viewer.onInputEnabled(false); + createSoftCursor(); // non-scaled cursor + } + } + + public void setPixelFormat() throws IOException { + bytesPixel = viewer.setPixelFormat(rfb); + + updateFramebufferSize(); + } + + public void updateFramebufferSize() { + + if(rfb == null) { + s_logger.warn("updateFrameBufferSize is called but RFB is not ready"); + return; + } + + // Useful shortcuts. + int fbWidth = rfb.framebufferWidth; + int fbHeight = rfb.framebufferHeight; + + // Calculate scaling factor for auto scaling. + if (maxWidth > 0 && maxHeight > 0) { + int f1 = maxWidth * 100 / fbWidth; + int f2 = maxHeight * 100 / fbHeight; + scalingFactor = Math.min(f1, f2); + if (scalingFactor > 100) + scalingFactor = 100; + + if(s_logger.isInfoEnabled()) + s_logger.info("Scaling desktop at " + scalingFactor + "%"); + } + + // Update scaled framebuffer geometry. + scaledWidth = (fbWidth * scalingFactor + 50) / 100; + scaledHeight = (fbHeight * scalingFactor + 50) / 100; + + // Create new off-screen image either if it does not exist, or if + // its geometry should be changed. It's not necessary to replace + // existing image if only pixel format should be changed. + if (memImage == null) { +// memImage = viewer.vncContainer.createImage(fbWidth, fbHeight); + memImage = new BufferedImage(fbWidth, fbHeight, BufferedImage.TYPE_4BYTE_ABGR); + memGraphics = memImage.getGraphics(); + } else if (memImage.getWidth(null) != fbWidth || memImage.getHeight(null) != fbHeight) { + synchronized(memImage) { +// memImage = viewer.vncContainer.createImage(fbWidth, fbHeight); + memImage = new BufferedImage(fbWidth, fbHeight, BufferedImage.TYPE_4BYTE_ABGR); + memGraphics = memImage.getGraphics(); + } + } + + // Images with raw pixels should be re-allocated on every change + // of geometry or pixel format. + if (bytesPixel == 1) { + + pixels24 = null; + pixels8 = new byte[fbWidth * fbHeight]; + + pixelsSource = new MemoryImageSource(fbWidth, fbHeight, cm8, pixels8, 0, fbWidth); + + zrleTilePixels24 = null; + zrleTilePixels8 = new byte[64 * 64]; + + } else { + + pixels8 = null; + pixels24 = new int[fbWidth * fbHeight]; + + pixelsSource = new MemoryImageSource(fbWidth, fbHeight, cm24, pixels24, 0, fbWidth); + + zrleTilePixels8 = null; + zrleTilePixels24 = new int[64 * 64]; + } + pixelsSource.setAnimated(true); + rawPixelsImage = Toolkit.getDefaultToolkit().createImage(pixelsSource); + + viewer.onFramebufferSizeChange(scaledWidth, scaledHeight); + } + + public void resizeDesktopFrame() { + setSize(scaledWidth, scaledHeight); + + // FIXME: Find a better way to determine correct size of a + // ScrollPane. -- const + viewer.onDesktopResize(); + + // Try to limit the frame size to the screen size. + Dimension screenSize = viewer.getScreenSize(); + Dimension frameSize = viewer.getFrameSize(); + Dimension newSize = frameSize; + + // Reduce Screen Size by 30 pixels in each direction; + // This is a (poor) attempt to account for + // 1) Menu bar on Macintosh (should really also account for + // Dock on OSX). Usually 22px on top of screen. + // 2) Taxkbar on Windows (usually about 28 px on bottom) + // 3) Other obstructions. + + screenSize.height -= 30; + screenSize.width -= 30; + + boolean needToResizeFrame = false; + if (frameSize.height > screenSize.height) { + newSize.height = screenSize.height; + needToResizeFrame = true; + } + if (frameSize.width > screenSize.width) { + newSize.width = screenSize.width; + needToResizeFrame = true; + } + + if (needToResizeFrame) + viewer.onFrameResize(newSize); + } + + // + // processNormalProtocol() - executed by the rfbThread to deal with the + // RFB socket. + // + + long timestamp = 0; + long bytes_sent_last = 0; + long rate_limit = 100000; + + public void processNormalProtocol() throws Exception { + try { + timestamp = 0; + // main dispatch loop + while (true) { + processNormalProtocol2(); + } + } catch (IOException e) { + + // Server connection get disrupted. + // Send a special msg to the client stream so that the client can blank out + // the screen. + viewer.onProtocolProcessException(e); + + // rethrow the exception; + throw e; + } + } + + private void processNormalProtocol2() throws Exception { + byte[] bs = rfb.sis.getSplit(); +// if (viewer.inProxyMode) { +// s_logger.info("bs != null = " + (bs != null) + " viewer.clientStream = " + viewer.clientStream + " info = " + viewer.clientStreamInfo); +// } + viewer.onPreProtocolProcess(bs); + + // Read message type from the server. + int msgType = rfb.readServerMessageType(); + + // Process the message depending on its type. + switch (msgType) { + case 250: + int b = rfb.is.read(); + if (b == 1) { + if(s_logger.isDebugEnabled()) + s_logger.debug("S->C RFB msg VMOps DISCONNECT"); + + /* + viewer.vc.paintErrorString("Disconnected"); + */ + viewer.onDisconnectMessage(); + break; + } else if (b == 2) { + int n = rfb.is.readInt(); + + if(s_logger.isDebugEnabled()) + s_logger.debug("S->C RFB msg VMOps COMPRESSED"); + + if(s_logger.isInfoEnabled()) + s_logger.info("Reading compressed data size=" + n); + + byte[] buf = new byte[n]; + rfb.is.readFully(buf); + ByteArrayInputStream bis = new ByteArrayInputStream(buf); + DataInputStream oldis = rfb.is; + // swap out the underlying input stream + rfb.is = new DataInputStream(new GZIPInputStream(bis, 65536)); + try { + processNormalProtocol2(); + } finally { + rfb.is = oldis; + } + break; + } else if (b == 3) { + if(s_logger.isDebugEnabled()) + s_logger.debug("S->C RFB msg VMOps KEEP-ALIVE"); + + // Send liveliness message + rfb.writeClientCustomMessage(0); + break; + } + throw new Exception ("Message type 250 comes with bad submessage type: " + b); + + case RfbProto.FramebufferUpdate: + if(s_logger.isDebugEnabled()) + s_logger.debug("S->C RFB FramebufferUpdate"); + + rfb.readFramebufferUpdate(); + boolean cursorPosReceived = false; + boolean fullUpdateNeeded = false; + for (int i = 0; i < rfb.updateNRects; i++) { + rfb.readFramebufferUpdateRectHdr(); + int rx = rfb.updateRectX, ry = rfb.updateRectY; + int rw = rfb.updateRectW, rh = rfb.updateRectH; + + if (rfb.updateRectEncoding == rfb.EncodingLastRect) { + if(s_logger.isInfoEnabled()) + s_logger.info("Received EncodingLastRect packet in iteration " + i + " of " + rfb.updateNRects); + break; + } + + if (rfb.updateRectEncoding == rfb.EncodingNewFBSize) { + rfb.setFramebufferSize(rw, rh); + updateFramebufferSize(); + + if(s_logger.isInfoEnabled()) + s_logger.info("Frame buffer size is changed to (" + rw + ", " + rh + "), packet in iteration " + i + " of " + rfb.updateNRects); + break; + } + + if (rfb.updateRectEncoding == rfb.EncodingXCursor || + rfb.updateRectEncoding == rfb.EncodingRichCursor) { + + byte[] cursorData = new byte[ConsoleCanvas.getCursorDataSize(rfb.updateRectEncoding, + rw, rh)]; + handleCursorShapeUpdate(rfb.updateRectEncoding, rx, ry, rw, rh, cursorData); + viewer.onFramebufferCursorShapeChange(rfb.updateRectEncoding, rx, ry, rw, rh, cursorData); + + if(s_logger.isInfoEnabled()) + s_logger.info("Received CursorShapeUpdate packet in iteration " + i + " of " + rfb.updateNRects); + continue; + } + + if (rfb.updateRectEncoding == rfb.EncodingPointerPos) { + softCursorMove(rx, ry); + cursorPosReceived = true; + viewer.onFramebufferCursorMove(rx, ry); + + if(s_logger.isInfoEnabled()) + s_logger.info("Received PointerPosUpdate packet in iteration " + i + " of " + rfb.updateNRects); + continue; + } + + // synchronize the access to memory pixel images as we have + // separated client update into a sender thread + rfb.startTiming(); + + // if(s_logger.isInfoEnabled()) + // s_logger.info("RFB Update Rect Encoding: " + rfb.updateRectEncoding); + switch (rfb.updateRectEncoding) { + case RfbProto.EncodingRaw: + handleRawRect(rx, ry, rw, rh); + break; + case RfbProto.EncodingCopyRect: + handleCopyRect(rx, ry, rw, rh); + break; + case RfbProto.EncodingRRE: + handleRRERect(rx, ry, rw, rh); + break; + case RfbProto.EncodingCoRRE: + handleCoRRERect(rx, ry, rw, rh); + break; + case RfbProto.EncodingHextile: + handleHextileRect(rx, ry, rw, rh); + break; + case RfbProto.EncodingZRLE: + handleZRLERect(rx, ry, rw, rh); + break; + case RfbProto.EncodingZlib: + handleZlibRect(rx, ry, rw, rh); + break; + case RfbProto.EncodingTight: + handleTightRect(rx, ry, rw, rh); + break; + default: + throw new Exception("Unknown RFB rectangle encoding " + + rfb.updateRectEncoding); + } + + viewer.onFramebufferUpdate(rx, ry, rw, rh); + rfb.stopTiming(); + + fullUpdateNeeded = viewer.onPostFrameBufferUpdateProcess(cursorPosReceived); + // viewer.autoSelectEncodings(); + } + + // send update request outside the for loop! + if(!viewer.isProxy()) { + if(s_logger.isDebugEnabled()) + s_logger.debug("Send FrameBufferUpdateRequest in response of server update return"); + + rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth, + rfb.framebufferHeight, !fullUpdateNeeded); + } else { + // if don't have client connected, to drive thumbnail updates, we need to continue + // to keep the request-cycle with server + if(!viewer.hasClientConnection()) { + if(s_logger.isDebugEnabled()) + s_logger.debug("No client is connected, send FrameBufferUpdateRequest from proxy"); + + rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth, + rfb.framebufferHeight, + !fullUpdateNeeded); + } else { + if(s_logger.isDebugEnabled()) + s_logger.debug("Client is connected, let client to send FrameBufferUpdateRequest"); + } + } + break; + + case RfbProto.SetColourMapEntries: + if(s_logger.isDebugEnabled()) + s_logger.debug("S->C RFB SetColourMapEntries"); + + throw new Exception("Can't handle SetColourMapEntries message"); + + case RfbProto.Bell: + if(s_logger.isDebugEnabled()) + s_logger.debug("S->C RFB Bell"); + + viewer.onBellMessage(); + break; + + case RfbProto.ServerCutText: + if(s_logger.isDebugEnabled()) + s_logger.debug("S->C RFB ServerCutText"); + + String s = rfb.readServerCutText(); + // viewer.clipboard.setCutText(s); + break; + + default: + if(s_logger.isDebugEnabled()) + s_logger.debug("S->C RFB Unknown message type " + msgType); + throw new Exception("Unknown RFB message type " + msgType); + } + } + + // + // Handle a raw rectangle. The second form with paint==false is used + // by the Hextile decoder for raw-encoded tiles. + // + void handleRawRect(int x, int y, int w, int h) throws IOException { + handleRawRect(x, y, w, h, true); + } + + void handleRawRect(int x, int y, int w, int h, boolean paint) throws IOException { + // s_logger.info("handleRawRect " + " x=" + x + " y=" + y + " w=" + w + " h=" + h + " paint=" + paint); + + if (bytesPixel == 1) { + for (int dy = y; dy < y + h; dy++) { + rfb.readFully(pixels8, dy * rfb.framebufferWidth + x, w); + if (rfb.rec != null) { + rfb.rec.write(pixels8, dy * rfb.framebufferWidth + x, w); + } + } + } else { + byte[] buf = new byte[w * 4]; + int i, offset; + for (int dy = y; dy < y + h; dy++) { + rfb.readFully(buf); + if (rfb.rec != null) { + rfb.rec.write(buf); + } + + offset = dy * rfb.framebufferWidth + x; + for (i = 0; i < w; i++) { + pixels24[offset + i] = + (buf[i * 4 + 2] & 0xFF) << 16 | + (buf[i * 4 + 1] & 0xFF) << 8 | + (buf[i * 4] & 0xFF); + } + } + } + + handleUpdatedPixels(x, y, w, h); + if (paint) + scheduleRepaint(x, y, w, h); + } + + // + // Handle a CopyRect rectangle. + // + void handleCopyRect(int x, int y, int w, int h) throws IOException { + + s_logger.info("handleCopyRect " + " x=" + x + " y=" + y + " w=" + w + " h=" + h); + rfb.readCopyRect(); + memGraphics.copyArea(rfb.copyRectSrcX, rfb.copyRectSrcY, w, h, + x - rfb.copyRectSrcX, y - rfb.copyRectSrcY); + + scheduleRepaint(x, y, w, h); + } + + // + // Handle an RRE-encoded rectangle. + // + void handleRRERect(int x, int y, int w, int h) throws IOException { + //s_logger.info("handleRRERect " + " x=" + x + " y=" + y + " w=" + w + " h=" + h); + + int nSubrects = rfb.is.readInt(); + + byte[] bg_buf = new byte[bytesPixel]; + rfb.readFully(bg_buf); + Color pixel; + if (bytesPixel == 1) { + pixel = colors[bg_buf[0] & 0xFF]; + } else { + pixel = new Color(bg_buf[2] & 0xFF, bg_buf[1] & 0xFF, bg_buf[0] & 0xFF); + } + memGraphics.setColor(pixel); + memGraphics.fillRect(x, y, w, h); + + byte[] buf = new byte[nSubrects * (bytesPixel + 8)]; + rfb.readFully(buf); + DataInputStream ds = new DataInputStream(new ByteArrayInputStream(buf)); + + if (rfb.rec != null) { + rfb.rec.writeIntBE(nSubrects); + rfb.rec.write(bg_buf); + rfb.rec.write(buf); + } + + int sx, sy, sw, sh; + + for (int j = 0; j < nSubrects; j++) { + if (bytesPixel == 1) { + pixel = colors[ds.readUnsignedByte()]; + } else { + ds.skip(4); + pixel = new Color(buf[j*12+2] & 0xFF, + buf[j*12+1] & 0xFF, + buf[j*12] & 0xFF); + } + sx = x + ds.readUnsignedShort(); + sy = y + ds.readUnsignedShort(); + sw = ds.readUnsignedShort(); + sh = ds.readUnsignedShort(); + + memGraphics.setColor(pixel); + memGraphics.fillRect(sx, sy, sw, sh); + } + + scheduleRepaint(x, y, w, h); + } + + // + // Handle a CoRRE-encoded rectangle. + // + + void handleCoRRERect(int x, int y, int w, int h) throws IOException { + //s_logger.info("handleCpRRERect " + " x=" + x + " y=" + y + " w=" + w + " h=" + h); + int nSubrects = rfb.is.readInt(); + + byte[] bg_buf = new byte[bytesPixel]; + rfb.readFully(bg_buf); + Color pixel; + if (bytesPixel == 1) { + pixel = colors[bg_buf[0] & 0xFF]; + } else { + pixel = new Color(bg_buf[2] & 0xFF, bg_buf[1] & 0xFF, bg_buf[0] & 0xFF); + } + memGraphics.setColor(pixel); + memGraphics.fillRect(x, y, w, h); + + byte[] buf = new byte[nSubrects * (bytesPixel + 4)]; + rfb.readFully(buf); + + if (rfb.rec != null) { + rfb.rec.writeIntBE(nSubrects); + rfb.rec.write(bg_buf); + rfb.rec.write(buf); + } + + int sx, sy, sw, sh; + int i = 0; + + for (int j = 0; j < nSubrects; j++) { + if (bytesPixel == 1) { + pixel = colors[buf[i++] & 0xFF]; + } else { + pixel = new Color(buf[i+2] & 0xFF, buf[i+1] & 0xFF, buf[i] & 0xFF); + i += 4; + } + sx = x + (buf[i++] & 0xFF); + sy = y + (buf[i++] & 0xFF); + sw = buf[i++] & 0xFF; + sh = buf[i++] & 0xFF; + + memGraphics.setColor(pixel); + memGraphics.fillRect(sx, sy, sw, sh); + } + + scheduleRepaint(x, y, w, h); + } + + // + // Handle a Hextile-encoded rectangle. + // + + // These colors should be kept between handleHextileSubrect() calls. + private Color hextile_bg, hextile_fg; + + void handleHextileRect(int x, int y, int w, int h) throws IOException { + // s_logger.info("handleHextileRect " + " x=" + x + " y=" + y + " w=" + w + " h=" + h); + hextile_bg = new Color(0); + hextile_fg = new Color(0); + + for (int ty = y; ty < y + h; ty += 16) { + int th = 16; + if (y + h - ty < 16) + th = y + h - ty; + + for (int tx = x; tx < x + w; tx += 16) { + int tw = 16; + if (x + w - tx < 16) + tw = x + w - tx; + + handleHextileSubrect(tx, ty, tw, th); + } + + // Finished with a row of tiles, now let's show it. + + } + scheduleRepaint(x, y, w, h); + } + + // + // Handle one tile in the Hextile-encoded data. + // + void handleHextileSubrect(int tx, int ty, int tw, int th) throws IOException { + + int subencoding = rfb.is.readUnsignedByte(); + if (rfb.rec != null) { + rfb.rec.writeByte(subencoding); + } + + // Is it a raw-encoded sub-rectangle? + if ((subencoding & rfb.HextileRaw) != 0) { + handleRawRect(tx, ty, tw, th, false); + return; + } + + // Read and draw the background if specified. + byte[] cbuf = new byte[bytesPixel]; + if ((subencoding & rfb.HextileBackgroundSpecified) != 0) { + rfb.readFully(cbuf); + if (bytesPixel == 1) { + hextile_bg = colors[cbuf[0] & 0xFF]; + } else { + hextile_bg = new Color(cbuf[2] & 0xFF, cbuf[1] & 0xFF, cbuf[0] & 0xFF); + } + + if (rfb.rec != null) { + rfb.rec.write(cbuf); + } + } + memGraphics.setColor(hextile_bg); + memGraphics.fillRect(tx, ty, tw, th); + + // Read the foreground color if specified. + if ((subencoding & rfb.HextileForegroundSpecified) != 0) { + rfb.readFully(cbuf); + if (bytesPixel == 1) { + hextile_fg = colors[cbuf[0] & 0xFF]; + } else { + hextile_fg = new Color(cbuf[2] & 0xFF, cbuf[1] & 0xFF, cbuf[0] & 0xFF); + } + if (rfb.rec != null) { + rfb.rec.write(cbuf); + } + } + + // Done with this tile if there is no sub-rectangles. + if ((subencoding & rfb.HextileAnySubrects) == 0) + return; + + int nSubrects = rfb.is.readUnsignedByte(); + int bufsize = nSubrects * 2; + if ((subencoding & rfb.HextileSubrectsColoured) != 0) { + bufsize += nSubrects * bytesPixel; + } + byte[] buf = new byte[bufsize]; + rfb.readFully(buf); + if (rfb.rec != null) { + rfb.rec.writeByte(nSubrects); + rfb.rec.write(buf); + } + + int b1, b2, sx, sy, sw, sh; + int i = 0; + if ((subencoding & rfb.HextileSubrectsColoured) == 0) { + // Sub-rectangles are all of the same color. + memGraphics.setColor(hextile_fg); + for (int j = 0; j < nSubrects; j++) { + b1 = buf[i++] & 0xFF; + b2 = buf[i++] & 0xFF; + sx = tx + (b1 >> 4); + sy = ty + (b1 & 0xf); + sw = (b2 >> 4) + 1; + sh = (b2 & 0xf) + 1; + memGraphics.fillRect(sx, sy, sw, sh); + } + } else if (bytesPixel == 1) { + + // BGR233 (8-bit color) version for colored sub-rectangles. + for (int j = 0; j < nSubrects; j++) { + hextile_fg = colors[buf[i++] & 0xFF]; + b1 = buf[i++] & 0xFF; + b2 = buf[i++] & 0xFF; + sx = tx + (b1 >> 4); + sy = ty + (b1 & 0xf); + sw = (b2 >> 4) + 1; + sh = (b2 & 0xf) + 1; + memGraphics.setColor(hextile_fg); + memGraphics.fillRect(sx, sy, sw, sh); + } + + } else { + + // Full-color (24-bit) version for colored sub-rectangles. + for (int j = 0; j < nSubrects; j++) { + hextile_fg = new Color(buf[i+2] & 0xFF, + buf[i+1] & 0xFF, + buf[i] & 0xFF); + i += 4; + b1 = buf[i++] & 0xFF; + b2 = buf[i++] & 0xFF; + sx = tx + (b1 >> 4); + sy = ty + (b1 & 0xf); + sw = (b2 >> 4) + 1; + sh = (b2 & 0xf) + 1; + memGraphics.setColor(hextile_fg); + memGraphics.fillRect(sx, sy, sw, sh); + } + + } + } + + // + // Handle a ZRLE-encoded rectangle. + // + // FIXME: Currently, session recording is not fully supported for ZRLE. + // + + void handleZRLERect(int x, int y, int w, int h) throws Exception { + //s_logger.info("handleZRLERect " + " x=" + x + " y=" + y + " w=" + w + " h=" + h); + if (zrleInStream == null) + zrleInStream = new ZlibInStream(); + + int nBytes = rfb.is.readInt(); + if (nBytes > 64 * 1024 * 1024) + throw new Exception("ZRLE decoder: illegal compressed data size"); + + if (zrleBuf == null || zrleBufLen < nBytes) { + zrleBufLen = nBytes + 4096; + zrleBuf = new byte[zrleBufLen]; + } + + // FIXME: Do not wait for all the data before decompression. + rfb.readFully(zrleBuf, 0, nBytes); + + if (rfb.rec != null) { + if (rfb.recordFromBeginning) { + rfb.rec.writeIntBE(nBytes); + rfb.rec.write(zrleBuf, 0, nBytes); + } else if (!zrleRecWarningShown) { + + if(s_logger.isInfoEnabled()) { + s_logger.info("Warning: ZRLE session can be recorded" + + " only from the beginning"); + s_logger.info("Warning: Recorded file may be corrupted"); + } + zrleRecWarningShown = true; + } + } + + zrleInStream.setUnderlying(new MemInStream(zrleBuf, 0, nBytes), nBytes); + + for (int ty = y; ty < y+h; ty += 64) { + + int th = Math.min(y+h-ty, 64); + + for (int tx = x; tx < x+w; tx += 64) { + + int tw = Math.min(x+w-tx, 64); + + int mode = zrleInStream.readU8(); + boolean rle = (mode & 128) != 0; + int palSize = mode & 127; + int[] palette = new int[128]; + + readZrlePalette(palette, palSize); + + if (palSize == 1) { + int pix = palette[0]; + Color c = (bytesPixel == 1) ? + colors[pix] : new Color(0xFF000000 | pix); + memGraphics.setColor(c); + memGraphics.fillRect(tx, ty, tw, th); + continue; + } + + if (!rle) { + if (palSize == 0) { + readZrleRawPixels(tw, th); + } else { + readZrlePackedPixels(tw, th, palette, palSize); + } + } else { + if (palSize == 0) { + readZrlePlainRLEPixels(tw, th); + } else { + readZrlePackedRLEPixels(tw, th, palette); + } + } + handleUpdatedZrleTile(tx, ty, tw, th); + } + } + + zrleInStream.reset(); + + scheduleRepaint(x, y, w, h); + } + + int readPixel(InStream is) throws Exception { + int pix; + if (bytesPixel == 1) { + pix = is.readU8(); + } else { + int p1 = is.readU8(); + int p2 = is.readU8(); + int p3 = is.readU8(); + pix = (p3 & 0xFF) << 16 | (p2 & 0xFF) << 8 | (p1 & 0xFF); + } + return pix; + } + + void readPixels(InStream is, int[] dst, int count) throws Exception { + int pix; + if (bytesPixel == 1) { + byte[] buf = new byte[count]; + is.readBytes(buf, 0, count); + for (int i = 0; i < count; i++) { + dst[i] = (int)buf[i] & 0xFF; + } + } else { + byte[] buf = new byte[count * 3]; + is.readBytes(buf, 0, count * 3); + for (int i = 0; i < count; i++) { + dst[i] = ((buf[i*3+2] & 0xFF) << 16 | + (buf[i*3+1] & 0xFF) << 8 | + (buf[i*3] & 0xFF)); + } + } + } + + void readZrlePalette(int[] palette, int palSize) throws Exception { + readPixels(zrleInStream, palette, palSize); + } + + void readZrleRawPixels(int tw, int th) throws Exception { + if (bytesPixel == 1) { + zrleInStream.readBytes(zrleTilePixels8, 0, tw * th); + } else { + readPixels(zrleInStream, zrleTilePixels24, tw * th); /// + } + } + + void readZrlePackedPixels(int tw, int th, int[] palette, int palSize) + throws Exception { + + int bppp = ((palSize > 16) ? 8 : + ((palSize > 4) ? 4 : ((palSize > 2) ? 2 : 1))); + int ptr = 0; + + for (int i = 0; i < th; i++) { + int eol = ptr + tw; + int b = 0; + int nbits = 0; + + while (ptr < eol) { + if (nbits == 0) { + b = zrleInStream.readU8(); + nbits = 8; + } + nbits -= bppp; + int index = (b >> nbits) & ((1 << bppp) - 1) & 127; + if (bytesPixel == 1) { + zrleTilePixels8[ptr++] = (byte)palette[index]; + } else { + zrleTilePixels24[ptr++] = palette[index]; + } + } + } + } + + void readZrlePlainRLEPixels(int tw, int th) throws Exception { + int ptr = 0; + int end = ptr + tw * th; + while (ptr < end) { + int pix = readPixel(zrleInStream); + int len = 1; + int b; + do { + b = zrleInStream.readU8(); + len += b; + } while (b == 255); + + if (!(len <= end - ptr)) + throw new Exception("ZRLE decoder: assertion failed" + + " (len <= end-ptr)"); + + if (bytesPixel == 1) { + while (len-- > 0) zrleTilePixels8[ptr++] = (byte)pix; + } else { + while (len-- > 0) zrleTilePixels24[ptr++] = pix; + } + } + } + + void readZrlePackedRLEPixels(int tw, int th, int[] palette) + throws Exception { + + int ptr = 0; + int end = ptr + tw * th; + while (ptr < end) { + int index = zrleInStream.readU8(); + int len = 1; + if ((index & 128) != 0) { + int b; + do { + b = zrleInStream.readU8(); + len += b; + } while (b == 255); + + if (!(len <= end - ptr)) + throw new Exception("ZRLE decoder: assertion failed" + + " (len <= end - ptr)"); + } + + index &= 127; + int pix = palette[index]; + + if (bytesPixel == 1) { + while (len-- > 0) zrleTilePixels8[ptr++] = (byte)pix; + } else { + while (len-- > 0) zrleTilePixels24[ptr++] = pix; + } + } + } + + // + // Copy pixels from zrleTilePixels8 or zrleTilePixels24, then update. + // + + void handleUpdatedZrleTile(int x, int y, int w, int h) { + Object src, dst; + if (bytesPixel == 1) { + src = zrleTilePixels8; dst = pixels8; + } else { + src = zrleTilePixels24; dst = pixels24; + } + int offsetSrc = 0; + int offsetDst = (y * rfb.framebufferWidth + x); + for (int j = 0; j < h; j++) { + System.arraycopy(src, offsetSrc, dst, offsetDst, w); + offsetSrc += w; + offsetDst += rfb.framebufferWidth; + } + handleUpdatedPixels(x, y, w, h); + } + + // + // Handle a Zlib-encoded rectangle. + // + + void handleZlibRect(int x, int y, int w, int h) throws Exception { + // s_logger.info("handleZlibRect " + " x=" + x + " y=" + y + " w=" + w + " h=" + h); + int nBytes = rfb.is.readInt(); + + if (zlibBuf == null || zlibBufLen < nBytes) { + zlibBufLen = nBytes * 2; + zlibBuf = new byte[zlibBufLen]; + } + + rfb.readFully(zlibBuf, 0, nBytes); + + if (rfb.rec != null && rfb.recordFromBeginning) { + rfb.rec.writeIntBE(nBytes); + rfb.rec.write(zlibBuf, 0, nBytes); + } + + if (zlibInflater == null) { + zlibInflater = new Inflater(); + } + zlibInflater.setInput(zlibBuf, 0, nBytes); + + if (bytesPixel == 1) { + for (int dy = y; dy < y + h; dy++) { + zlibInflater.inflate(pixels8, dy * rfb.framebufferWidth + x, w); + if (rfb.rec != null && !rfb.recordFromBeginning) + rfb.rec.write(pixels8, dy * rfb.framebufferWidth + x, w); + } + } else { + byte[] buf = new byte[w * 4]; + int i, offset; + for (int dy = y; dy < y + h; dy++) { + zlibInflater.inflate(buf); + offset = dy * rfb.framebufferWidth + x; + for (i = 0; i < w; i++) { + pixels24[offset + i] = + (buf[i * 4 + 2] & 0xFF) << 16 | + (buf[i * 4 + 1] & 0xFF) << 8 | + (buf[i * 4] & 0xFF); + } + if (rfb.rec != null && !rfb.recordFromBeginning) + rfb.rec.write(buf); + } + } + + handleUpdatedPixels(x, y, w, h); + scheduleRepaint(x, y, w, h); + } + + // + // Handle a Tight-encoded rectangle. + // + + void handleTightRect(int x, int y, int w, int h) throws Exception { + //s_logger.info("handleTightRect " + " x=" + x + " y=" + y + " w=" + w + " h=" + h); + int comp_ctl = rfb.is.readUnsignedByte(); + if (rfb.rec != null) { + if (rfb.recordFromBeginning || + comp_ctl == (rfb.TightFill << 4) || + comp_ctl == (rfb.TightJpeg << 4)) { + // Send data exactly as received. + rfb.rec.writeByte(comp_ctl); + } else { + // Tell the decoder to flush each of the four zlib streams. + rfb.rec.writeByte(comp_ctl | 0x0F); + } + } + + // Flush zlib streams if we are told by the server to do so. + for (int stream_id = 0; stream_id < 4; stream_id++) { + if ((comp_ctl & 1) != 0 && tightInflaters[stream_id] != null) { + tightInflaters[stream_id] = null; + } + comp_ctl >>= 1; + } + + // Check correctness of subencoding value. + if (comp_ctl > rfb.TightMaxSubencoding) { + throw new Exception("Incorrect tight subencoding: " + comp_ctl); + } + + // Handle solid-color rectangles. + if (comp_ctl == rfb.TightFill) { + + if (bytesPixel == 1) { + int idx = rfb.is.readUnsignedByte(); + memGraphics.setColor(colors[idx]); + if (rfb.rec != null) { + rfb.rec.writeByte(idx); + } + } else { + byte[] buf = new byte[3]; + rfb.readFully(buf); + if (rfb.rec != null) { + rfb.rec.write(buf); + } + Color bg = new Color(0xFF000000 | (buf[0] & 0xFF) << 16 | + (buf[1] & 0xFF) << 8 | (buf[2] & 0xFF)); + memGraphics.setColor(bg); + } + memGraphics.fillRect(x, y, w, h); + scheduleRepaint(x, y, w, h); + return; + + } + + if (comp_ctl == rfb.TightJpeg) { + + // Read JPEG data. + byte[] jpegData = new byte[rfb.readCompactLen()]; + rfb.readFully(jpegData); + if (rfb.rec != null) { + if (!rfb.recordFromBeginning) { + rfb.recordCompactLen(jpegData.length); + } + rfb.rec.write(jpegData); + } + + // Create an Image object from the JPEG data. + Image jpegImage = Toolkit.getDefaultToolkit().createImage(jpegData); + + // Remember the rectangle where the image should be drawn. + jpegRect = new Rectangle(x, y, w, h); + + // Let the imageUpdate() method do the actual drawing, here just + // wait until the image is fully loaded and drawn. + synchronized(jpegRect) { + Toolkit.getDefaultToolkit().prepareImage(jpegImage, -1, -1, this); + try { + // Wait no longer than three seconds. + jpegRect.wait(3000); + } catch (InterruptedException e) { + throw new Exception("Interrupted while decoding JPEG image"); + } + } + + // Done, jpegRect is not needed any more. + jpegRect = null; + return; + } + + // Read filter id and parameters. + int numColors = 0, rowSize = w; + byte[] palette8 = new byte[2]; + int[] palette24 = new int[256]; + boolean useGradient = false; + if ((comp_ctl & rfb.TightExplicitFilter) != 0) { + int filter_id = rfb.is.readUnsignedByte(); + if (rfb.rec != null) { + rfb.rec.writeByte(filter_id); + } + if (filter_id == rfb.TightFilterPalette) { + numColors = rfb.is.readUnsignedByte() + 1; + if (rfb.rec != null) { + rfb.rec.writeByte(numColors - 1); + } + if (bytesPixel == 1) { + if (numColors != 2) { + throw new Exception("Incorrect tight palette size: " + numColors); + } + rfb.readFully(palette8); + if (rfb.rec != null) { + rfb.rec.write(palette8); + } + } else { + byte[] buf = new byte[numColors * 3]; + rfb.readFully(buf); + if (rfb.rec != null) { + rfb.rec.write(buf); + } + for (int i = 0; i < numColors; i++) { + palette24[i] = ((buf[i * 3] & 0xFF) << 16 | + (buf[i * 3 + 1] & 0xFF) << 8 | + (buf[i * 3 + 2] & 0xFF)); + } + } + if (numColors == 2) + rowSize = (w + 7) / 8; + } else if (filter_id == rfb.TightFilterGradient) { + useGradient = true; + } else if (filter_id != rfb.TightFilterCopy) { + throw new Exception("Incorrect tight filter id: " + filter_id); + } + } + if (numColors == 0 && bytesPixel == 4) + rowSize *= 3; + + // Read, optionally uncompress and decode data. + int dataSize = h * rowSize; + if (dataSize < rfb.TightMinToCompress) { + // Data size is small - not compressed with zlib. + if (numColors != 0) { + // Indexed colors. + byte[] indexedData = new byte[dataSize]; + rfb.readFully(indexedData); + if (rfb.rec != null) { + rfb.rec.write(indexedData); + } + if (numColors == 2) { + // Two colors. + if (bytesPixel == 1) { + decodeMonoData(x, y, w, h, indexedData, palette8); + } else { + decodeMonoData(x, y, w, h, indexedData, palette24); + } + } else { + // 3..255 colors (assuming bytesPixel == 4). + int i = 0; + for (int dy = y; dy < y + h; dy++) { + for (int dx = x; dx < x + w; dx++) { + pixels24[dy * rfb.framebufferWidth + dx] = + palette24[indexedData[i++] & 0xFF]; + } + } + } + } else if (useGradient) { + // "Gradient"-processed data + byte[] buf = new byte[w * h * 3]; + rfb.readFully(buf); + if (rfb.rec != null) { + rfb.rec.write(buf); + } + decodeGradientData(x, y, w, h, buf); + } else { + // Raw truecolor data. + if (bytesPixel == 1) { + for (int dy = y; dy < y + h; dy++) { + rfb.readFully(pixels8, dy * rfb.framebufferWidth + x, w); + if (rfb.rec != null) { + rfb.rec.write(pixels8, dy * rfb.framebufferWidth + x, w); + } + } + } else { + byte[] buf = new byte[w * 3]; + int i, offset; + for (int dy = y; dy < y + h; dy++) { + rfb.readFully(buf); + if (rfb.rec != null) { + rfb.rec.write(buf); + } + offset = dy * rfb.framebufferWidth + x; + for (i = 0; i < w; i++) { + pixels24[offset + i] = + (buf[i * 3] & 0xFF) << 16 | + (buf[i * 3 + 1] & 0xFF) << 8 | + (buf[i * 3 + 2] & 0xFF); + } + } + } + } + } else { + // Data was compressed with zlib. + int zlibDataLen = rfb.readCompactLen(); + byte[] zlibData = new byte[zlibDataLen]; + rfb.readFully(zlibData); + if (rfb.rec != null && rfb.recordFromBeginning) { + rfb.rec.write(zlibData); + } + int stream_id = comp_ctl & 0x03; + if (tightInflaters[stream_id] == null) { + tightInflaters[stream_id] = new Inflater(); + } + Inflater myInflater = tightInflaters[stream_id]; + myInflater.setInput(zlibData); + byte[] buf = new byte[dataSize]; + myInflater.inflate(buf); + if (rfb.rec != null && !rfb.recordFromBeginning) { + rfb.recordCompressedData(buf); + } + + if (numColors != 0) { + // Indexed colors. + if (numColors == 2) { + // Two colors. + if (bytesPixel == 1) { + decodeMonoData(x, y, w, h, buf, palette8); + } else { + decodeMonoData(x, y, w, h, buf, palette24); + } + } else { + // More than two colors (assuming bytesPixel == 4). + int i = 0; + for (int dy = y; dy < y + h; dy++) { + for (int dx = x; dx < x + w; dx++) { + pixels24[dy * rfb.framebufferWidth + dx] = + palette24[buf[i++] & 0xFF]; + } + } + } + } else if (useGradient) { + // Compressed "Gradient"-filtered data (assuming bytesPixel == 4). + decodeGradientData(x, y, w, h, buf); + } else { + // Compressed truecolor data. + if (bytesPixel == 1) { + int destOffset = y * rfb.framebufferWidth + x; + for (int dy = 0; dy < h; dy++) { + System.arraycopy(buf, dy * w, pixels8, destOffset, w); + destOffset += rfb.framebufferWidth; + } + } else { + int srcOffset = 0; + int destOffset, i; + for (int dy = 0; dy < h; dy++) { + myInflater.inflate(buf); + destOffset = (y + dy) * rfb.framebufferWidth + x; + for (i = 0; i < w; i++) { + pixels24[destOffset + i] = + (buf[srcOffset] & 0xFF) << 16 | + (buf[srcOffset + 1] & 0xFF) << 8 | + (buf[srcOffset + 2] & 0xFF); + srcOffset += 3; + } + } + } + } + } + + handleUpdatedPixels(x, y, w, h); + scheduleRepaint(x, y, w, h); + } + + // + // Decode 1bpp-encoded bi-color rectangle (8-bit and 24-bit versions). + // + + void decodeMonoData(int x, int y, int w, int h, byte[] src, byte[] palette) { + + int dx, dy, n; + int i = y * rfb.framebufferWidth + x; + int rowBytes = (w + 7) / 8; + byte b; + + for (dy = 0; dy < h; dy++) { + for (dx = 0; dx < w / 8; dx++) { + b = src[dy*rowBytes+dx]; + for (n = 7; n >= 0; n--) + pixels8[i++] = palette[b >> n & 1]; + } + for (n = 7; n >= 8 - w % 8; n--) { + pixels8[i++] = palette[src[dy*rowBytes+dx] >> n & 1]; + } + i += (rfb.framebufferWidth - w); + } + } + + void decodeMonoData(int x, int y, int w, int h, byte[] src, int[] palette) { + + int dx, dy, n; + int i = y * rfb.framebufferWidth + x; + int rowBytes = (w + 7) / 8; + byte b; + + for (dy = 0; dy < h; dy++) { + for (dx = 0; dx < w / 8; dx++) { + b = src[dy*rowBytes+dx]; + for (n = 7; n >= 0; n--) + pixels24[i++] = palette[b >> n & 1]; + } + for (n = 7; n >= 8 - w % 8; n--) { + pixels24[i++] = palette[src[dy*rowBytes+dx] >> n & 1]; + } + i += (rfb.framebufferWidth - w); + } + } + + // + // Decode data processed with the "Gradient" filter. + // + + void decodeGradientData (int x, int y, int w, int h, byte[] buf) { + + int dx, dy, c; + byte[] prevRow = new byte[w * 3]; + byte[] thisRow = new byte[w * 3]; + byte[] pix = new byte[3]; + int[] est = new int[3]; + + int offset = y * rfb.framebufferWidth + x; + + for (dy = 0; dy < h; dy++) { + + /* First pixel in a row */ + for (c = 0; c < 3; c++) { + pix[c] = (byte)(prevRow[c] + buf[dy * w * 3 + c]); + thisRow[c] = pix[c]; + } + pixels24[offset++] = + (pix[0] & 0xFF) << 16 | (pix[1] & 0xFF) << 8 | (pix[2] & 0xFF); + + /* Remaining pixels of a row */ + for (dx = 1; dx < w; dx++) { + for (c = 0; c < 3; c++) { + est[c] = ((prevRow[dx * 3 + c] & 0xFF) + (pix[c] & 0xFF) - + (prevRow[(dx-1) * 3 + c] & 0xFF)); + if (est[c] > 0xFF) { + est[c] = 0xFF; + } else if (est[c] < 0x00) { + est[c] = 0x00; + } + pix[c] = (byte)(est[c] + buf[(dy * w + dx) * 3 + c]); + thisRow[dx * 3 + c] = pix[c]; + } + pixels24[offset++] = + (pix[0] & 0xFF) << 16 | (pix[1] & 0xFF) << 8 | (pix[2] & 0xFF); + } + + System.arraycopy(thisRow, 0, prevRow, 0, w * 3); + offset += (rfb.framebufferWidth - w); + } + } + + // + // Display newly updated area of pixels. + // + + void handleUpdatedPixels(int x, int y, int w, int h) { + + pixelsSource.newPixels(x, y, w, h); + memGraphics.setClip(x, y, w, h); + memGraphics.drawImage(rawPixelsImage, 0, 0, null); + memGraphics.setClip(0, 0, rfb.framebufferWidth, rfb.framebufferHeight); + } + + // + // Tell JVM to repaint specified desktop area. + // + + void scheduleRepaint(int x, int y, int w, int h) { + if (rfb.framebufferWidth == scaledWidth) { + repaint(viewer.getDeferScreenUpdateTimeout(), x, y, w, h); + } else { + int sx = x * scalingFactor / 100; + int sy = y * scalingFactor / 100; + int sw = ((x + w) * scalingFactor + 49) / 100 - sx + 1; + int sh = ((y + h) * scalingFactor + 49) / 100 - sy + 1; + repaint(viewer.getDeferScreenUpdateTimeout(), sx, sy, sw, sh); + } + } + + // + // Handle events. + // + + public void keyPressed(KeyEvent evt) { + if (rfb != null) + processLocalKeyEvent(evt); + } + public void keyReleased(KeyEvent evt) { + if (rfb != null) + processLocalKeyEvent(evt); + } + public void keyTyped(KeyEvent evt) { + if (rfb != null) + evt.consume(); + } + + public void mousePressed(MouseEvent evt) { + if (rfb != null) + processLocalMouseEvent(evt, false); + } + public void mouseReleased(MouseEvent evt) { + if (rfb != null) + processLocalMouseEvent(evt, false); + } + public void mouseMoved(MouseEvent evt) { + if (rfb != null) + processLocalMouseEvent(evt, true); + } + public void mouseDragged(MouseEvent evt) { + if (rfb != null) + processLocalMouseEvent(evt, true); + } + + public void sendCtrlAltDel() { + KeyEvent evt = new KeyEvent(this, KeyEvent.KEY_PRESSED, System.currentTimeMillis(), + InputEvent.CTRL_MASK | InputEvent.ALT_MASK, 127); + + boolean bCloseRfb = false; + synchronized (rfb) { + try { + rfb.writeKeyEvent(evt); + } catch (IOException e) { + // Just close the underlying stream so the main loop will error out. + bCloseRfb = true; + } + rfb.notify(); + } + + if(bCloseRfb) { + synchronized (viewer) { + if (rfb != null) + rfb.close(); + } + } + } + + public void processLocalKeyEvent(KeyEvent evt) { + if (/*viewer.rfb != null*/ viewer.getRfb() != null && rfb.inNormalProtocol) { + if (!inputEnabled) { + if ((evt.getKeyChar() == 'r' || evt.getKeyChar() == 'R') + && evt.getID() == KeyEvent.KEY_PRESSED) { + // Request screen update. + try { + rfb.writeFramebufferUpdateRequest(0, 0, + rfb.framebufferWidth, rfb.framebufferHeight, + false); + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + // Input enabled. + boolean bCloseRfb = false; + synchronized (rfb) { + try { + // Convert Ctrl-Alt-Ins to Ctrl-Alt-Del + if (evt.getKeyCode() == 155 + && evt.getModifiers() == (InputEvent.CTRL_MASK | InputEvent.ALT_MASK)) { + evt.setKeyCode(127); + } + rfb.writeKeyEvent(evt); + } catch (IOException e) { + // Just close the underlying stream so the main loop will error out. + bCloseRfb = true; + } + rfb.notify(); + } + + if(bCloseRfb) { + synchronized (viewer) { + if (rfb != null) + rfb.close(); + } + } + } + } + // Don't ever pass keyboard events to AWT for default processing. + // Otherwise, pressing Tab would switch focus to ButtonPanel etc. + evt.consume(); + } + + public void processLocalMouseEvent(MouseEvent evt, boolean moved) { + if (/*viewer.rfb != null*/ viewer.getRfb() != null && rfb.inNormalProtocol) { + if (moved) { + softCursorMove(evt.getX(), evt.getY()); + } + if (rfb.framebufferWidth != scaledWidth) { + int sx = (evt.getX() * 100 + scalingFactor/2) / scalingFactor; + int sy = (evt.getY() * 100 + scalingFactor/2) / scalingFactor; + evt.translatePoint(sx - evt.getX(), sy - evt.getY()); + } + + boolean bCloseRfb = false; + synchronized(rfb) { + try { + rfb.writePointerEvent(evt); + } catch (Exception e) { + bCloseRfb = true; + } + rfb.notify(); + } + + if(bCloseRfb) { + synchronized (viewer) { + if (rfb != null) + rfb.close(); + } + } + } + } + + // + // Ignored events. + // + + public void mouseClicked(MouseEvent evt) {} + public void mouseEntered(MouseEvent evt) {} + public void mouseExited(MouseEvent evt) {} + + + ////////////////////////////////////////////////////////////////// + // + // Handle cursor shape updates (XCursor and RichCursor encodings). + // + + boolean showSoftCursor = false; + + MemoryImageSource softCursorSource; + Image softCursor; + + int cursorX = 0, cursorY = 0; + int cursorWidth, cursorHeight; + int origCursorWidth, origCursorHeight; + int hotX, hotY; + int origHotX, origHotY; + + // + // Handle cursor shape update (XCursor and RichCursor encodings). + // + + static int getCursorDataSize(int encodingType, int width, int height) + { + if(width * height == 0) + return 0; + + int bytesPerRow = (width + 7) / 8; + int bytesMaskData = bytesPerRow * height; + + if (encodingType == RfbProto.EncodingXCursor) { + return 6 + bytesMaskData * 2; + } else { + return width * height + bytesMaskData; + } + } + + synchronized void handleCursorShapeUpdate(int encodingType, + int xhot, int yhot, int width, int height, byte[] captureBuffer) + throws IOException { + + softCursorFree(); + + if (width * height == 0) + return; + + // Ignore cursor shape data if requested by user. + if (/*viewer.options.ignoreCursorUpdates*/ viewer.ignoreCursorUpdate()) { + int bytesPerRow = (width + 7) / 8; + int bytesMaskData = bytesPerRow * height; + + if (encodingType == rfb.EncodingXCursor) { + // rfb.is.skipBytes(6 + bytesMaskData * 2); + + rfb.is.readFully(captureBuffer); + + } else { + // rfb.EncodingRichCursor + // rfb.is.skipBytes(width * height + bytesMaskData); + + rfb.is.readFully(captureBuffer); + } + return; + } + + // Decode cursor pixel data. + softCursorSource = decodeCursorShape(encodingType, width, height, captureBuffer); + + // Set original (non-scaled) cursor dimensions. + origCursorWidth = width; + origCursorHeight = height; + origHotX = xhot; + origHotY = yhot; + + // Create off-screen cursor image. + createSoftCursor(); + + // Show the cursor. + showSoftCursor = true; + repaint(viewer.getDeferCursorUpdateTimeout(), + cursorX - hotX, cursorY - hotY, cursorWidth, cursorHeight); + } + + // + // decodeCursorShape(). Decode cursor pixel data and return + // corresponding MemoryImageSource instance. + // + + synchronized MemoryImageSource decodeCursorShape(int encodingType, int width, int height, byte[] captureBuffer) + throws IOException { + + ByteArrayOutputStream bos = new ByteArrayOutputStream(captureBuffer.length); + + int bytesPerRow = (width + 7) / 8; + int bytesMaskData = bytesPerRow * height; + + int[] softCursorPixels = new int[width * height]; + + if (encodingType == rfb.EncodingXCursor) { + + // Read foreground and background colors of the cursor. + byte[] rgb = new byte[6]; + rfb.readFully(rgb); + bos.write(rgb); + int[] colors = { (0xFF000000 | (rgb[3] & 0xFF) << 16 | + (rgb[4] & 0xFF) << 8 | (rgb[5] & 0xFF)), + (0xFF000000 | (rgb[0] & 0xFF) << 16 | + (rgb[1] & 0xFF) << 8 | (rgb[2] & 0xFF)) }; + + // Read pixel and mask data. + byte[] pixBuf = new byte[bytesMaskData]; + rfb.readFully(pixBuf); + bos.write(pixBuf); + + byte[] maskBuf = new byte[bytesMaskData]; + rfb.readFully(maskBuf); + bos.write(maskBuf); + + // Decode pixel data into softCursorPixels[]. + byte pixByte, maskByte; + int x, y, n, result; + int i = 0; + for (y = 0; y < height; y++) { + for (x = 0; x < width / 8; x++) { + pixByte = pixBuf[y * bytesPerRow + x]; + maskByte = maskBuf[y * bytesPerRow + x]; + for (n = 7; n >= 0; n--) { + if ((maskByte >> n & 1) != 0) { + result = colors[pixByte >> n & 1]; + } else { + result = 0; // Transparent pixel + } + softCursorPixels[i++] = result; + } + } + for (n = 7; n >= 8 - width % 8; n--) { + if ((maskBuf[y * bytesPerRow + x] >> n & 1) != 0) { + result = colors[pixBuf[y * bytesPerRow + x] >> n & 1]; + } else { + result = 0; // Transparent pixel + } + softCursorPixels[i++] = result; + } + } + } else { + // encodingType == rfb.EncodingRichCursor + + // Read pixel and mask data. + byte[] pixBuf = new byte[width * height * bytesPixel]; + rfb.readFully(pixBuf); + bos.write(pixBuf); + + byte[] maskBuf = new byte[bytesMaskData]; + rfb.readFully(maskBuf); + bos.write(maskBuf); + + // Decode pixel data into softCursorPixels[]. + byte pixByte, maskByte; + int x, y, n, result; + int i = 0; + for (y = 0; y < height; y++) { + for (x = 0; x < width / 8; x++) { + maskByte = maskBuf[y * bytesPerRow + x]; + for (n = 7; n >= 0; n--) { + if ((maskByte >> n & 1) != 0) { + if (bytesPixel == 1) { + result = cm8.getRGB(pixBuf[i]); + } else { + result = 0xFF000000 | + (pixBuf[i * 4 + 2] & 0xFF) << 16 | + (pixBuf[i * 4 + 1] & 0xFF) << 8 | + (pixBuf[i * 4] & 0xFF); + } + } else { + result = 0; // Transparent pixel + } + softCursorPixels[i++] = result; + } + } + for (n = 7; n >= 8 - width % 8; n--) { + if ((maskBuf[y * bytesPerRow + x] >> n & 1) != 0) { + if (bytesPixel == 1) { + result = cm8.getRGB(pixBuf[i]); + } else { + result = 0xFF000000 | + (pixBuf[i * 4 + 2] & 0xFF) << 16 | + (pixBuf[i * 4 + 1] & 0xFF) << 8 | + (pixBuf[i * 4] & 0xFF); + } + } else { + result = 0; // Transparent pixel + } + softCursorPixels[i++] = result; + } + } + } + + System.arraycopy(bos.toByteArray(), 0, captureBuffer, 0, captureBuffer.length); + return new MemoryImageSource(width, height, softCursorPixels, 0, width); + } + + // + // createSoftCursor(). Assign softCursor new Image (scaled if necessary). + // Uses softCursorSource as a source for new cursor image. + // + + synchronized void createSoftCursor() { + + if (softCursorSource == null) + return; + +/* + int scaleCursor = viewer.options.scaleCursor; +*/ + int scaleCursor = viewer.getCursorScaleFactor(); + if (scaleCursor == 0 || !inputEnabled) + scaleCursor = 100; + + // Save original cursor coordinates. + int x = cursorX - hotX; + int y = cursorY - hotY; + int w = cursorWidth; + int h = cursorHeight; + + cursorWidth = (origCursorWidth * scaleCursor + 50) / 100; + cursorHeight = (origCursorHeight * scaleCursor + 50) / 100; + hotX = (origHotX * scaleCursor + 50) / 100; + hotY = (origHotY * scaleCursor + 50) / 100; + softCursor = Toolkit.getDefaultToolkit().createImage(softCursorSource); + + if (scaleCursor != 100) { + softCursor = softCursor.getScaledInstance(cursorWidth, cursorHeight, + Image.SCALE_SMOOTH); + } + + if (showSoftCursor) { + // Compute screen area to update. + x = Math.min(x, cursorX - hotX); + y = Math.min(y, cursorY - hotY); + w = Math.max(w, cursorWidth); + h = Math.max(h, cursorHeight); + + /* + repaint(viewer.deferCursorUpdates, x, y, w, h); + */ + repaint(viewer.getDeferCursorUpdateTimeout(), x, y, w, h); + } + } + + // + // softCursorMove(). Moves soft cursor into a particular location. + // + + synchronized void softCursorMove(int x, int y) { + int oldX = cursorX; + int oldY = cursorY; + cursorX = x; + cursorY = y; + if (showSoftCursor) { + repaint(viewer.getDeferCursorUpdateTimeout(), + oldX - hotX, oldY - hotY, cursorWidth, cursorHeight); + repaint(viewer.getDeferCursorUpdateTimeout(), + cursorX - hotX, cursorY - hotY, cursorWidth, cursorHeight); + + } + } + + // + // softCursorFree(). Remove soft cursor, dispose resources. + // + public synchronized void softCursorFree() { + if (showSoftCursor) { + showSoftCursor = false; + softCursor = null; + softCursorSource = null; + + repaint(viewer.getDeferCursorUpdateTimeout(), + cursorX - hotX, cursorY - hotY, cursorWidth, cursorHeight); + } + } + + public void paintErrorString(String msg) { + if(memGraphics != null) { + memGraphics.setColor(Color.BLACK); + memGraphics.fillRect(0,0,memImage.getWidth(null),memImage.getHeight(null)); + memGraphics.setColor(Color.WHITE); + memGraphics.setFont(new Font(null, Font.PLAIN, 20)); + FontMetrics fm = memGraphics.getFontMetrics(); + int width = fm.stringWidth(msg); + int startx = (memImage.getWidth(null) - width) / 2; + if (startx < 0) startx = 0; + memGraphics.drawString(msg, startx, memImage.getHeight(null) / 2); + scheduleRepaint(0,0,memImage.getWidth(null),memImage.getHeight(null)); + } + } + + public void encodeFramebufferResize(int w, int h, OutputStream os) throws IOException { + DataOutputStream dos; + if(os instanceof DataOutputStream) + dos = (DataOutputStream)os; + else + dos = new DataOutputStream(os); + + dos.writeByte(RfbProto.FramebufferUpdate); + dos.writeByte(0); // padding + dos.writeShort(1); + dos.writeShort(0); + dos.writeShort(0); + dos.writeShort(w); + dos.writeShort(h); + dos.writeInt(RfbProto.EncodingNewFBSize); + } + + public void encodeCursorMove(int x, int y, OutputStream os) throws IOException { + DataOutputStream dos; + if(os instanceof DataOutputStream) + dos = (DataOutputStream)os; + else + dos = new DataOutputStream(os); + + dos.writeShort(x); + dos.writeShort(y); + dos.writeShort(0); + dos.writeShort(0); + dos.writeInt(RfbProto.EncodingPointerPos); + } + + public void encodeCursorShapeUpdate(int encodingType, + int xhot, int yhot, int width, int height, byte[] cursorData, OutputStream os) throws IOException { + + DataOutputStream dos; + if(os instanceof DataOutputStream) + dos = (DataOutputStream)os; + else + dos = new DataOutputStream(os); + + dos.writeShort(xhot); + dos.writeShort(yhot); + dos.writeShort(width); + dos.writeShort(height); + dos.writeInt(encodingType); + os.write(cursorData); + } + + public void encodeRawRectFrameUpdate(int x, int y, int w, int h, OutputStream os) throws IOException { + + DataOutputStream dos; + if(os instanceof DataOutputStream) + dos = (DataOutputStream)os; + else + dos = new DataOutputStream(os); + + PixelGrabber grabber = + new PixelGrabber(memImage, 0, 0, -1, -1, false); + + try { + if(grabber.grabPixels()) { + dos.writeShort(x); + dos.writeShort(y); + dos.writeShort(w); + dos.writeShort(h); + dos.writeInt(RfbProto.EncodingRaw); + + if (bytesPixel == 1) { + byte[] data = (byte[])grabber.getPixels(); + for (int dy = y; dy < y + h; dy++) { + os.write(data, dy * rfb.framebufferWidth + x, w); + } + } else { + int[] data = (int[])grabber.getPixels(); + + int i, offset; + for (int dy = y; dy < y + h; dy++) { + offset = dy * rfb.framebufferWidth + x; + for (i = 0; i < w; i++) { + int pixel = data[offset + i]; + dos.write(pixel & 0xFF); + dos.write((pixel >> 8)& 0xFF); + dos.write((pixel >> 16)& 0xFF); + dos.write((pixel >> 24)& 0xFF); + } + } + } + } + } catch (InterruptedException e) { + s_logger.warn("Exception whiel grabbering pixel data from memImage"); + } + } +} + diff --git a/console/src/com/cloud/console/ConsoleCanvas2.java b/console/src/com/cloud/console/ConsoleCanvas2.java new file mode 100644 index 00000000000..5ed2659636f --- /dev/null +++ b/console/src/com/cloud/console/ConsoleCanvas2.java @@ -0,0 +1,63 @@ +// +// Copyright (C) 2006 Constantin Kaplinsky. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +package com.cloud.console; + +import java.awt.*; +import java.io.*; + +// +// VncCanvas2 is a special version of VncCanvas which may use Java 2 API. +// +@SuppressWarnings("serial") +public class ConsoleCanvas2 extends ConsoleCanvas { + + public ConsoleCanvas2(RfbViewer v) throws IOException { + super(v); + disableFocusTraversalKeys(); + } + + public ConsoleCanvas2(RfbViewer v, int maxWidth_, int maxHeight_) + { + super(v, maxWidth_, maxHeight_); + disableFocusTraversalKeys(); + } + + public void paintScaledFrameBuffer(Graphics g) { + Graphics2D g2d = (Graphics2D)g; + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + g2d.drawImage(memImage, 0, 0, scaledWidth, scaledHeight, null); + } + + // + // Try to disable focus traversal keys (JVMs 1.4 and higher). + // + + private void disableFocusTraversalKeys() { + try { + Class[] argClasses = { Boolean.TYPE }; + java.lang.reflect.Method method = + getClass().getMethod("setFocusTraversalKeysEnabled", argClasses); + Object[] argObjects = { new Boolean(false) }; + method.invoke(this, argObjects); + } catch (Exception e) {} + } +} + diff --git a/console/src/com/cloud/console/DesCipher.java b/console/src/com/cloud/console/DesCipher.java new file mode 100644 index 00000000000..8f7a8c0a7a2 --- /dev/null +++ b/console/src/com/cloud/console/DesCipher.java @@ -0,0 +1,496 @@ +// +// This DES class has been extracted from package Acme.Crypto for use in VNC. +// The bytebit[] array has been reversed so that the most significant bit +// in each byte of the key is ignored, not the least significant. Also the +// unnecessary odd parity code has been removed. +// +// These changes are: +// Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// + +// DesCipher - the DES encryption method +// +// The meat of this code is by Dave Zimmerman , and is: +// +// Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved. +// +// Permission to use, copy, modify, and distribute this software +// and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and +// without fee is hereby granted, provided that this copyright notice is kept +// intact. +// +// WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY +// OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE +// FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR +// DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. +// +// THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE +// CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE +// PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT +// NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE +// SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE +// SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE +// PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP +// SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR +// HIGH RISK ACTIVITIES. +// +// +// The rest is: +// +// Copyright (C) 1996 by Jef Poskanzer . All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +/// The DES encryption method. +//

+// This is surprisingly fast, for pure Java. On a SPARC 20, wrapped +// in Acme.Crypto.EncryptedOutputStream or Acme.Crypto.EncryptedInputStream, +// it does around 7000 bytes/second. +//

+// Most of this code is by Dave Zimmerman , and is +// Copyright (c) 1996 Widget Workshop, Inc. See the source file for details. +//

+// Fetch the software.
+// Fetch the entire Acme package. +//

+// @see Des3Cipher +// @see EncryptedOutputStream +// @see EncryptedInputStream + +public class DesCipher + { + + // Constructor, byte-array key. + public DesCipher( byte[] key ) + { + setKey( key ); + } + + // Key routines. + + private int[] encryptKeys = new int[32]; + private int[] decryptKeys = new int[32]; + + /// Set the key. + public void setKey( byte[] key ) + { + deskey( key, true, encryptKeys ); + deskey( key, false, decryptKeys ); + } + + // Turn an 8-byte key into internal keys. + private void deskey( byte[] keyBlock, boolean encrypting, int[] KnL ) + { + int i, j, l, m, n; + int[] pc1m = new int[56]; + int[] pcr = new int[56]; + int[] kn = new int[32]; + + for ( j = 0; j < 56; ++j ) + { + l = pc1[j]; + m = l & 07; + pc1m[j] = ( (keyBlock[l >>> 3] & bytebit[m]) != 0 )? 1: 0; + } + + for ( i = 0; i < 16; ++i ) + { + if ( encrypting ) + m = i << 1; + else + m = (15-i) << 1; + n = m+1; + kn[m] = kn[n] = 0; + for ( j = 0; j < 28; ++j ) + { + l = j+totrot[i]; + if ( l < 28 ) + pcr[j] = pc1m[l]; + else + pcr[j] = pc1m[l-28]; + } + for ( j=28; j < 56; ++j ) + { + l = j+totrot[i]; + if ( l < 56 ) + pcr[j] = pc1m[l]; + else + pcr[j] = pc1m[l-28]; + } + for ( j = 0; j < 24; ++j ) + { + if ( pcr[pc2[j]] != 0 ) + kn[m] |= bigbyte[j]; + if ( pcr[pc2[j+24]] != 0 ) + kn[n] |= bigbyte[j]; + } + } + cookey( kn, KnL ); + } + + private void cookey( int[] raw, int KnL[] ) + { + int raw0, raw1; + int rawi, KnLi; + int i; + + for ( i = 0, rawi = 0, KnLi = 0; i < 16; ++i ) + { + raw0 = raw[rawi++]; + raw1 = raw[rawi++]; + KnL[KnLi] = (raw0 & 0x00fc0000) << 6; + KnL[KnLi] |= (raw0 & 0x00000fc0) << 10; + KnL[KnLi] |= (raw1 & 0x00fc0000) >>> 10; + KnL[KnLi] |= (raw1 & 0x00000fc0) >>> 6; + ++KnLi; + KnL[KnLi] = (raw0 & 0x0003f000) << 12; + KnL[KnLi] |= (raw0 & 0x0000003f) << 16; + KnL[KnLi] |= (raw1 & 0x0003f000) >>> 4; + KnL[KnLi] |= (raw1 & 0x0000003f); + ++KnLi; + } + } + + + // Block encryption routines. + + private int[] tempInts = new int[2]; + + /// Encrypt a block of eight bytes. + public void encrypt( byte[] clearText, int clearOff, byte[] cipherText, int cipherOff ) + { + squashBytesToInts( clearText, clearOff, tempInts, 0, 2 ); + des( tempInts, tempInts, encryptKeys ); + spreadIntsToBytes( tempInts, 0, cipherText, cipherOff, 2 ); + } + + /// Decrypt a block of eight bytes. + public void decrypt( byte[] cipherText, int cipherOff, byte[] clearText, int clearOff ) + { + squashBytesToInts( cipherText, cipherOff, tempInts, 0, 2 ); + des( tempInts, tempInts, decryptKeys ); + spreadIntsToBytes( tempInts, 0, clearText, clearOff, 2 ); + } + + // The DES function. + private void des( int[] inInts, int[] outInts, int[] keys ) + { + int fval, work, right, leftt; + int round; + int keysi = 0; + + leftt = inInts[0]; + right = inInts[1]; + + work = ((leftt >>> 4) ^ right) & 0x0f0f0f0f; + right ^= work; + leftt ^= (work << 4); + + work = ((leftt >>> 16) ^ right) & 0x0000ffff; + right ^= work; + leftt ^= (work << 16); + + work = ((right >>> 2) ^ leftt) & 0x33333333; + leftt ^= work; + right ^= (work << 2); + + work = ((right >>> 8) ^ leftt) & 0x00ff00ff; + leftt ^= work; + right ^= (work << 8); + right = (right << 1) | ((right >>> 31) & 1); + + work = (leftt ^ right) & 0xaaaaaaaa; + leftt ^= work; + right ^= work; + leftt = (leftt << 1) | ((leftt >>> 31) & 1); + + for ( round = 0; round < 8; ++round ) + { + work = (right << 28) | (right >>> 4); + work ^= keys[keysi++]; + fval = SP7[ work & 0x0000003f ]; + fval |= SP5[(work >>> 8) & 0x0000003f ]; + fval |= SP3[(work >>> 16) & 0x0000003f ]; + fval |= SP1[(work >>> 24) & 0x0000003f ]; + work = right ^ keys[keysi++]; + fval |= SP8[ work & 0x0000003f ]; + fval |= SP6[(work >>> 8) & 0x0000003f ]; + fval |= SP4[(work >>> 16) & 0x0000003f ]; + fval |= SP2[(work >>> 24) & 0x0000003f ]; + leftt ^= fval; + work = (leftt << 28) | (leftt >>> 4); + work ^= keys[keysi++]; + fval = SP7[ work & 0x0000003f ]; + fval |= SP5[(work >>> 8) & 0x0000003f ]; + fval |= SP3[(work >>> 16) & 0x0000003f ]; + fval |= SP1[(work >>> 24) & 0x0000003f ]; + work = leftt ^ keys[keysi++]; + fval |= SP8[ work & 0x0000003f ]; + fval |= SP6[(work >>> 8) & 0x0000003f ]; + fval |= SP4[(work >>> 16) & 0x0000003f ]; + fval |= SP2[(work >>> 24) & 0x0000003f ]; + right ^= fval; + } + + right = (right << 31) | (right >>> 1); + work = (leftt ^ right) & 0xaaaaaaaa; + leftt ^= work; + right ^= work; + leftt = (leftt << 31) | (leftt >>> 1); + work = ((leftt >>> 8) ^ right) & 0x00ff00ff; + right ^= work; + leftt ^= (work << 8); + work = ((leftt >>> 2) ^ right) & 0x33333333; + right ^= work; + leftt ^= (work << 2); + work = ((right >>> 16) ^ leftt) & 0x0000ffff; + leftt ^= work; + right ^= (work << 16); + work = ((right >>> 4) ^ leftt) & 0x0f0f0f0f; + leftt ^= work; + right ^= (work << 4); + outInts[0] = right; + outInts[1] = leftt; + } + + + // Tables, permutations, S-boxes, etc. + + private static byte[] bytebit = { + (byte)0x01, (byte)0x02, (byte)0x04, (byte)0x08, + (byte)0x10, (byte)0x20, (byte)0x40, (byte)0x80 + }; + private static int[] bigbyte = { + 0x800000, 0x400000, 0x200000, 0x100000, + 0x080000, 0x040000, 0x020000, 0x010000, + 0x008000, 0x004000, 0x002000, 0x001000, + 0x000800, 0x000400, 0x000200, 0x000100, + 0x000080, 0x000040, 0x000020, 0x000010, + 0x000008, 0x000004, 0x000002, 0x000001 + }; + private static byte[] pc1 = { + (byte)56, (byte)48, (byte)40, (byte)32, (byte)24, (byte)16, (byte) 8, + (byte) 0, (byte)57, (byte)49, (byte)41, (byte)33, (byte)25, (byte)17, + (byte) 9, (byte) 1, (byte)58, (byte)50, (byte)42, (byte)34, (byte)26, + (byte)18, (byte)10, (byte) 2, (byte)59, (byte)51, (byte)43, (byte)35, + (byte)62, (byte)54, (byte)46, (byte)38, (byte)30, (byte)22, (byte)14, + (byte) 6, (byte)61, (byte)53, (byte)45, (byte)37, (byte)29, (byte)21, + (byte)13, (byte) 5, (byte)60, (byte)52, (byte)44, (byte)36, (byte)28, + (byte)20, (byte)12, (byte) 4, (byte)27, (byte)19, (byte)11, (byte)3 + }; + private static int[] totrot = { + 1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28 + }; + + private static byte[] pc2 = { + (byte)13, (byte)16, (byte)10, (byte)23, (byte) 0, (byte) 4, + (byte) 2, (byte)27, (byte)14, (byte) 5, (byte)20, (byte) 9, + (byte)22, (byte)18, (byte)11, (byte)3 , (byte)25, (byte) 7, + (byte)15, (byte) 6, (byte)26, (byte)19, (byte)12, (byte) 1, + (byte)40, (byte)51, (byte)30, (byte)36, (byte)46, (byte)54, + (byte)29, (byte)39, (byte)50, (byte)44, (byte)32, (byte)47, + (byte)43, (byte)48, (byte)38, (byte)55, (byte)33, (byte)52, + (byte)45, (byte)41, (byte)49, (byte)35, (byte)28, (byte)31, + }; + + private static int[] SP1 = { + 0x01010400, 0x00000000, 0x00010000, 0x01010404, + 0x01010004, 0x00010404, 0x00000004, 0x00010000, + 0x00000400, 0x01010400, 0x01010404, 0x00000400, + 0x01000404, 0x01010004, 0x01000000, 0x00000004, + 0x00000404, 0x01000400, 0x01000400, 0x00010400, + 0x00010400, 0x01010000, 0x01010000, 0x01000404, + 0x00010004, 0x01000004, 0x01000004, 0x00010004, + 0x00000000, 0x00000404, 0x00010404, 0x01000000, + 0x00010000, 0x01010404, 0x00000004, 0x01010000, + 0x01010400, 0x01000000, 0x01000000, 0x00000400, + 0x01010004, 0x00010000, 0x00010400, 0x01000004, + 0x00000400, 0x00000004, 0x01000404, 0x00010404, + 0x01010404, 0x00010004, 0x01010000, 0x01000404, + 0x01000004, 0x00000404, 0x00010404, 0x01010400, + 0x00000404, 0x01000400, 0x01000400, 0x00000000, + 0x00010004, 0x00010400, 0x00000000, 0x01010004 + }; + private static int[] SP2 = { + 0x80108020, 0x80008000, 0x00008000, 0x00108020, + 0x00100000, 0x00000020, 0x80100020, 0x80008020, + 0x80000020, 0x80108020, 0x80108000, 0x80000000, + 0x80008000, 0x00100000, 0x00000020, 0x80100020, + 0x00108000, 0x00100020, 0x80008020, 0x00000000, + 0x80000000, 0x00008000, 0x00108020, 0x80100000, + 0x00100020, 0x80000020, 0x00000000, 0x00108000, + 0x00008020, 0x80108000, 0x80100000, 0x00008020, + 0x00000000, 0x00108020, 0x80100020, 0x00100000, + 0x80008020, 0x80100000, 0x80108000, 0x00008000, + 0x80100000, 0x80008000, 0x00000020, 0x80108020, + 0x00108020, 0x00000020, 0x00008000, 0x80000000, + 0x00008020, 0x80108000, 0x00100000, 0x80000020, + 0x00100020, 0x80008020, 0x80000020, 0x00100020, + 0x00108000, 0x00000000, 0x80008000, 0x00008020, + 0x80000000, 0x80100020, 0x80108020, 0x00108000 + }; + private static int[] SP3 = { + 0x00000208, 0x08020200, 0x00000000, 0x08020008, + 0x08000200, 0x00000000, 0x00020208, 0x08000200, + 0x00020008, 0x08000008, 0x08000008, 0x00020000, + 0x08020208, 0x00020008, 0x08020000, 0x00000208, + 0x08000000, 0x00000008, 0x08020200, 0x00000200, + 0x00020200, 0x08020000, 0x08020008, 0x00020208, + 0x08000208, 0x00020200, 0x00020000, 0x08000208, + 0x00000008, 0x08020208, 0x00000200, 0x08000000, + 0x08020200, 0x08000000, 0x00020008, 0x00000208, + 0x00020000, 0x08020200, 0x08000200, 0x00000000, + 0x00000200, 0x00020008, 0x08020208, 0x08000200, + 0x08000008, 0x00000200, 0x00000000, 0x08020008, + 0x08000208, 0x00020000, 0x08000000, 0x08020208, + 0x00000008, 0x00020208, 0x00020200, 0x08000008, + 0x08020000, 0x08000208, 0x00000208, 0x08020000, + 0x00020208, 0x00000008, 0x08020008, 0x00020200 + }; + private static int[] SP4 = { + 0x00802001, 0x00002081, 0x00002081, 0x00000080, + 0x00802080, 0x00800081, 0x00800001, 0x00002001, + 0x00000000, 0x00802000, 0x00802000, 0x00802081, + 0x00000081, 0x00000000, 0x00800080, 0x00800001, + 0x00000001, 0x00002000, 0x00800000, 0x00802001, + 0x00000080, 0x00800000, 0x00002001, 0x00002080, + 0x00800081, 0x00000001, 0x00002080, 0x00800080, + 0x00002000, 0x00802080, 0x00802081, 0x00000081, + 0x00800080, 0x00800001, 0x00802000, 0x00802081, + 0x00000081, 0x00000000, 0x00000000, 0x00802000, + 0x00002080, 0x00800080, 0x00800081, 0x00000001, + 0x00802001, 0x00002081, 0x00002081, 0x00000080, + 0x00802081, 0x00000081, 0x00000001, 0x00002000, + 0x00800001, 0x00002001, 0x00802080, 0x00800081, + 0x00002001, 0x00002080, 0x00800000, 0x00802001, + 0x00000080, 0x00800000, 0x00002000, 0x00802080 + }; + private static int[] SP5 = { + 0x00000100, 0x02080100, 0x02080000, 0x42000100, + 0x00080000, 0x00000100, 0x40000000, 0x02080000, + 0x40080100, 0x00080000, 0x02000100, 0x40080100, + 0x42000100, 0x42080000, 0x00080100, 0x40000000, + 0x02000000, 0x40080000, 0x40080000, 0x00000000, + 0x40000100, 0x42080100, 0x42080100, 0x02000100, + 0x42080000, 0x40000100, 0x00000000, 0x42000000, + 0x02080100, 0x02000000, 0x42000000, 0x00080100, + 0x00080000, 0x42000100, 0x00000100, 0x02000000, + 0x40000000, 0x02080000, 0x42000100, 0x40080100, + 0x02000100, 0x40000000, 0x42080000, 0x02080100, + 0x40080100, 0x00000100, 0x02000000, 0x42080000, + 0x42080100, 0x00080100, 0x42000000, 0x42080100, + 0x02080000, 0x00000000, 0x40080000, 0x42000000, + 0x00080100, 0x02000100, 0x40000100, 0x00080000, + 0x00000000, 0x40080000, 0x02080100, 0x40000100 + }; + private static int[] SP6 = { + 0x20000010, 0x20400000, 0x00004000, 0x20404010, + 0x20400000, 0x00000010, 0x20404010, 0x00400000, + 0x20004000, 0x00404010, 0x00400000, 0x20000010, + 0x00400010, 0x20004000, 0x20000000, 0x00004010, + 0x00000000, 0x00400010, 0x20004010, 0x00004000, + 0x00404000, 0x20004010, 0x00000010, 0x20400010, + 0x20400010, 0x00000000, 0x00404010, 0x20404000, + 0x00004010, 0x00404000, 0x20404000, 0x20000000, + 0x20004000, 0x00000010, 0x20400010, 0x00404000, + 0x20404010, 0x00400000, 0x00004010, 0x20000010, + 0x00400000, 0x20004000, 0x20000000, 0x00004010, + 0x20000010, 0x20404010, 0x00404000, 0x20400000, + 0x00404010, 0x20404000, 0x00000000, 0x20400010, + 0x00000010, 0x00004000, 0x20400000, 0x00404010, + 0x00004000, 0x00400010, 0x20004010, 0x00000000, + 0x20404000, 0x20000000, 0x00400010, 0x20004010 + }; + private static int[] SP7 = { + 0x00200000, 0x04200002, 0x04000802, 0x00000000, + 0x00000800, 0x04000802, 0x00200802, 0x04200800, + 0x04200802, 0x00200000, 0x00000000, 0x04000002, + 0x00000002, 0x04000000, 0x04200002, 0x00000802, + 0x04000800, 0x00200802, 0x00200002, 0x04000800, + 0x04000002, 0x04200000, 0x04200800, 0x00200002, + 0x04200000, 0x00000800, 0x00000802, 0x04200802, + 0x00200800, 0x00000002, 0x04000000, 0x00200800, + 0x04000000, 0x00200800, 0x00200000, 0x04000802, + 0x04000802, 0x04200002, 0x04200002, 0x00000002, + 0x00200002, 0x04000000, 0x04000800, 0x00200000, + 0x04200800, 0x00000802, 0x00200802, 0x04200800, + 0x00000802, 0x04000002, 0x04200802, 0x04200000, + 0x00200800, 0x00000000, 0x00000002, 0x04200802, + 0x00000000, 0x00200802, 0x04200000, 0x00000800, + 0x04000002, 0x04000800, 0x00000800, 0x00200002 + }; + private static int[] SP8 = { + 0x10001040, 0x00001000, 0x00040000, 0x10041040, + 0x10000000, 0x10001040, 0x00000040, 0x10000000, + 0x00040040, 0x10040000, 0x10041040, 0x00041000, + 0x10041000, 0x00041040, 0x00001000, 0x00000040, + 0x10040000, 0x10000040, 0x10001000, 0x00001040, + 0x00041000, 0x00040040, 0x10040040, 0x10041000, + 0x00001040, 0x00000000, 0x00000000, 0x10040040, + 0x10000040, 0x10001000, 0x00041040, 0x00040000, + 0x00041040, 0x00040000, 0x10041000, 0x00001000, + 0x00000040, 0x10040040, 0x00001000, 0x00041040, + 0x10001000, 0x00000040, 0x10000040, 0x10040000, + 0x10040040, 0x10000000, 0x00040000, 0x10001040, + 0x00000000, 0x10041040, 0x00040040, 0x10000040, + 0x10040000, 0x10001000, 0x10001040, 0x00000000, + 0x10041040, 0x00041000, 0x00041000, 0x00001040, + 0x00001040, 0x00040040, 0x10000000, 0x10041000 + }; + + // Routines taken from other parts of the Acme utilities. + + /// Squash bytes down to ints. + public static void squashBytesToInts( byte[] inBytes, int inOff, int[] outInts, int outOff, int intLen ) + { + for ( int i = 0; i < intLen; ++i ) + outInts[outOff + i] = + ( ( inBytes[inOff + i * 4 ] & 0xff ) << 24 ) | + ( ( inBytes[inOff + i * 4 + 1] & 0xff ) << 16 ) | + ( ( inBytes[inOff + i * 4 + 2] & 0xff ) << 8 ) | + ( inBytes[inOff + i * 4 + 3] & 0xff ); + } + + /// Spread ints into bytes. + public static void spreadIntsToBytes( int[] inInts, int inOff, byte[] outBytes, int outOff, int intLen ) + { + for ( int i = 0; i < intLen; ++i ) + { + outBytes[outOff + i * 4 ] = (byte) ( inInts[inOff + i] >>> 24 ); + outBytes[outOff + i * 4 + 1] = (byte) ( inInts[inOff + i] >>> 16 ); + outBytes[outOff + i * 4 + 2] = (byte) ( inInts[inOff + i] >>> 8 ); + outBytes[outOff + i * 4 + 3] = (byte) inInts[inOff + i]; + } + } + } diff --git a/console/src/com/cloud/console/ITileScanListener.java b/console/src/com/cloud/console/ITileScanListener.java new file mode 100644 index 00000000000..9d0b5bd2f2c --- /dev/null +++ b/console/src/com/cloud/console/ITileScanListener.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.console; + +import java.awt.Rectangle; +import java.util.List; + +public interface ITileScanListener { + boolean onTileChange(Rectangle rowMergedRect, int row, int col); + void onRegionChange(List regionList); +} diff --git a/console/src/com/cloud/console/InStream.java b/console/src/com/cloud/console/InStream.java new file mode 100644 index 00000000000..ba3868f1368 --- /dev/null +++ b/console/src/com/cloud/console/InStream.java @@ -0,0 +1,180 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +// +// rdr::InStream marshalls data from a buffer stored in RDR (RFB Data +// Representation). +// + +abstract public class InStream { + + // check() ensures there is buffer data for at least one item of size + // itemSize bytes. Returns the number of items in the buffer (up to a + // maximum of nItems). + + public final int check(int itemSize, int nItems) throws Exception { + if (ptr + itemSize * nItems > end) { + if (ptr + itemSize > end) + return overrun(itemSize, nItems); + + nItems = (end - ptr) / itemSize; + } + return nItems; + } + + public final void check(int itemSize) throws Exception { + if (ptr + itemSize > end) + overrun(itemSize, 1); + } + + // readU/SN() methods read unsigned and signed N-bit integers. + + public final int readS8() throws Exception { + check(1); return b[ptr++]; + } + + public final int readS16() throws Exception { + check(2); int b0 = b[ptr++]; + int b1 = b[ptr++] & 0xff; return b0 << 8 | b1; + } + + public final int readS32() throws Exception { + check(4); int b0 = b[ptr++]; + int b1 = b[ptr++] & 0xff; + int b2 = b[ptr++] & 0xff; + int b3 = b[ptr++] & 0xff; + return b0 << 24 | b1 << 16 | b2 << 8 | b3; + } + + public final int readU8() throws Exception { + return readS8() & 0xff; + } + + public final int readU16() throws Exception { + return readS16() & 0xffff; + } + + public final int readU32() throws Exception { + return readS32() & 0xffffffff; + } + + // readString() reads a string - a U32 length followed by the data. + + public final String readString() throws Exception { + int len = readU32(); + if (len > maxStringLength) + throw new Exception("InStream max string length exceeded"); + + char[] str = new char[len]; + int i = 0; + while (i < len) { + int j = i + check(1, len - i); + while (i < j) { + str[i++] = (char)b[ptr++]; + } + } + + return new String(str); + } + + // maxStringLength protects against allocating a huge buffer. Set it + // higher if you need longer strings. + + public static int maxStringLength = 65535; + + public final void skip(int bytes) throws Exception { + while (bytes > 0) { + int n = check(1, bytes); + ptr += n; + bytes -= n; + } + } + + // readBytes() reads an exact number of bytes into an array at an offset. + + public void readBytes(byte[] data, int offset, int length) throws Exception { + int offsetEnd = offset + length; + while (offset < offsetEnd) { + int n = check(1, offsetEnd - offset); + System.arraycopy(b, ptr, data, offset, n); + ptr += n; + offset += n; + } + } + + // readOpaqueN() reads a quantity "without byte-swapping". Because java has + // no byte-ordering, we just use big-endian. + + public final int readOpaque8() throws Exception { + return readU8(); + } + + public final int readOpaque16() throws Exception { + return readU16(); + } + + public final int readOpaque32() throws Exception { + return readU32(); + } + + public final int readOpaque24A() throws Exception { + check(3); int b0 = b[ptr++]; + int b1 = b[ptr++]; int b2 = b[ptr++]; + return b0 << 24 | b1 << 16 | b2 << 8; + } + + public final int readOpaque24B() throws Exception { + check(3); int b0 = b[ptr++]; + int b1 = b[ptr++]; int b2 = b[ptr++]; + return b0 << 16 | b1 << 8 | b2; + } + + // pos() returns the position in the stream. + + abstract public int pos(); + + // bytesAvailable() returns true if at least one byte can be read from the + // stream without blocking. i.e. if false is returned then readU8() would + // block. + + public boolean bytesAvailable() { return end != ptr; } + + // getbuf(), getptr(), getend() and setptr() are "dirty" methods which allow + // you to manipulate the buffer directly. This is useful for a stream which + // is a wrapper around an underlying stream. + + public final byte[] getbuf() { return b; } + public final int getptr() { return ptr; } + public final int getend() { return end; } + public final void setptr(int p) { ptr = p; } + + // overrun() is implemented by a derived class to cope with buffer overrun. + // It ensures there are at least itemSize bytes of buffer data. Returns + // the number of items in the buffer (up to a maximum of nItems). itemSize + // is supposed to be "small" (a few bytes). + + abstract protected int overrun(int itemSize, int nItems) throws Exception; + + protected InStream() {} + protected byte[] b; + protected int ptr; + protected int end; +} diff --git a/console/src/com/cloud/console/Logger.java b/console/src/com/cloud/console/Logger.java new file mode 100644 index 00000000000..593e4e5c57b --- /dev/null +++ b/console/src/com/cloud/console/Logger.java @@ -0,0 +1,225 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.console; + +// logger facility for dynamic switch between console logger used in Applet and log4j based logger +public class Logger { + private static LoggerFactory factory = null; + + public static final int LEVEL_TRACE = 1; + public static final int LEVEL_DEBUG = 2; + public static final int LEVEL_INFO = 3; + public static final int LEVEL_WARN = 4; + public static final int LEVEL_ERROR = 5; + + private Class clazz; + private Logger logger; + + private static int level = LEVEL_INFO; + + public static Logger getLogger(Class clazz) { + return new Logger(clazz); + } + + public static void setFactory(LoggerFactory f) { + factory = f; + } + + public static void setLevel(int l) { + level = l; + } + + public Logger(Class clazz) { + this.clazz = clazz; + } + + protected Logger() { + } + + public boolean isTraceEnabled() { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + return logger.isTraceEnabled(); + } + return level <= LEVEL_TRACE; + } + + public boolean isDebugEnabled() { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + return logger.isDebugEnabled(); + } + return level <= LEVEL_DEBUG; + } + + public boolean isInfoEnabled() { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + return logger.isInfoEnabled(); + } + return level <= LEVEL_INFO; + } + + public void trace(Object message) { + + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + logger.trace(message); + } else { + if(level <= LEVEL_TRACE) + System.out.println(message); + } + } + + public void trace(Object message, Throwable exception) { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + logger.trace(message, exception); + } else { + if(level <= LEVEL_TRACE) { + System.out.println(message); + if (exception != null) { + exception.printStackTrace(System.out); + } + } + } + } + + public void info(Object message) { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + logger.info(message); + } else { + if(level <= LEVEL_INFO) + System.out.println(message); + } + } + + public void info(Object message, Throwable exception) { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + logger.info(message, exception); + } else { + if(level <= LEVEL_INFO) { + System.out.println(message); + if (exception != null) { + exception.printStackTrace(System.out); + } + } + } + } + + public void debug(Object message) { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + logger.debug(message); + } else { + if(level <= LEVEL_DEBUG) + System.out.println(message); + } + } + + public void debug(Object message, Throwable exception) { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + logger.debug(message, exception); + } else { + if(level <= LEVEL_DEBUG) { + System.out.println(message); + if (exception != null) { + exception.printStackTrace(System.out); + } + } + } + } + + public void warn(Object message) { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + logger.warn(message); + } else { + if(level <= LEVEL_WARN) + System.out.println(message); + } + } + + public void warn(Object message, Throwable exception) { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + logger.warn(message, exception); + } else { + if(level <= LEVEL_WARN) { + System.out.println(message); + if (exception != null) { + exception.printStackTrace(System.out); + } + } + } + } + + public void error(Object message) { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + logger.error(message); + } else { + if(level <= LEVEL_ERROR) + System.out.println(message); + } + } + + public void error(Object message, Throwable exception) { + if(factory != null) { + if(logger == null) + logger = factory.getLogger(clazz); + + logger.error(message, exception); + } else { + if(level <= LEVEL_ERROR) { + System.out.println(message); + if (exception != null) { + exception.printStackTrace(System.out); + } + } + } + } +} diff --git a/console/src/com/cloud/console/LoggerFactory.java b/console/src/com/cloud/console/LoggerFactory.java new file mode 100644 index 00000000000..0c78dda6382 --- /dev/null +++ b/console/src/com/cloud/console/LoggerFactory.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.console; + +public interface LoggerFactory { + Logger getLogger(Class clazz); +} diff --git a/console/src/com/cloud/console/MemInStream.java b/console/src/com/cloud/console/MemInStream.java new file mode 100644 index 00000000000..130f8d5d8de --- /dev/null +++ b/console/src/com/cloud/console/MemInStream.java @@ -0,0 +1,35 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +public class MemInStream extends InStream { + + public MemInStream(byte[] data, int offset, int len) { + b = data; + ptr = offset; + end = offset + len; + } + + public int pos() { return ptr; } + + protected int overrun(int itemSize, int nItems) throws Exception { + throw new Exception("MemInStream overrun: end of stream"); + } +} diff --git a/console/src/com/cloud/console/Region.java b/console/src/com/cloud/console/Region.java new file mode 100644 index 00000000000..65e031293b5 --- /dev/null +++ b/console/src/com/cloud/console/Region.java @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.console; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.List; + +public class Region { + private Rectangle bound; + private List rectList; + + public Region() { + bound = new Rectangle(0, 0, 0, 0); + rectList = new ArrayList(); + } + + public Region(Rectangle rect) { + bound = new Rectangle(rect.x, rect.y, rect.width, rect.height); + rectList = new ArrayList(); + rectList.add(rect); + } + + public Rectangle getBound() { + return bound; + } + + public void clearBound() { + assert(rectList.size() == 0); + bound.x = bound.y = bound.width = bound.height = 0; + } + + public List getRectangles() { + return rectList; + } + + public boolean add(Rectangle rect) { + if(bound.isEmpty()) { + assert(rectList.size() == 0); + bound.x = rect.x; + bound.y = rect.y; + bound.width = rect.width; + bound.height = rect.height; + + rectList.add(rect); + return true; + } + + Rectangle rcInflated = new Rectangle(rect.x - 1, rect.y- 1, rect.width + 2, rect.height + 2); + if(!bound.intersects(rcInflated)) + return false; + + for(Rectangle r : rectList) { + if(r.intersects(rcInflated)) { + if(!r.contains(rect)) { + enlargeBound(rect); + rectList.add(rect); + return true; + } + } + } + return false; + } + + private void enlargeBound(Rectangle rect) { + int boundLeft = Math.min(bound.x, rect.x); + int boundTop = Math.min(bound.y, rect.y); + int boundRight = Math.max(bound.x + bound.width, rect.x + rect.width); + int boundBottom = Math.max(bound.y + bound.height, rect.y + rect.height); + + bound.x = boundLeft; + bound.y = boundTop; + bound.width = boundRight - boundLeft; + bound.height = boundBottom - boundTop; + } +} diff --git a/console/src/com/cloud/console/RegionClassifier.java b/console/src/com/cloud/console/RegionClassifier.java new file mode 100644 index 00000000000..b24a22244e8 --- /dev/null +++ b/console/src/com/cloud/console/RegionClassifier.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.console; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.List; + +public class RegionClassifier { + private List regionList; + + public RegionClassifier() { + regionList = new ArrayList(); + } + + public void add(Rectangle rect) { + // quickly identify that if we need a new region + boolean newRegion = true; + Rectangle rcInflated = new Rectangle(rect.x - 1, rect.y - 1, rect.width + 2, rect.height + 2); + for(Region region : regionList) { + if(region.getBound().intersects(rcInflated)) { + newRegion = false; + break; + } + } + + if(newRegion) { + regionList.add(new Region(rect)); + } else { + for(Region region : regionList) { + if(region.add(rect)) + return; + } + regionList.add(new Region(rect)); + } + } + + public List getRegionList() { + return regionList; + } + + public void clear() { + regionList.clear(); + } +} diff --git a/console/src/com/cloud/console/RfbProto.java b/console/src/com/cloud/console/RfbProto.java new file mode 100644 index 00000000000..db0bec159e2 --- /dev/null +++ b/console/src/com/cloud/console/RfbProto.java @@ -0,0 +1,1474 @@ +// +// Copyright (C) 2001-2004 HorizonLive.com, Inc. All Rights Reserved. +// Copyright (C) 2001-2006 Constantin Kaplinsky. All Rights Reserved. +// Copyright (C) 2000 Tridia Corporation. All Rights Reserved. +// Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +// +// RfbProto.java +// + +import java.io.*; +import java.awt.event.*; +import java.net.Socket; +import java.util.zip.*; + +public class RfbProto { + + private static final Logger s_logger = Logger.getLogger(RfbProto.class); + + public final static String + versionMsg_3_3 = "RFB 003.003\n", + versionMsg_3_7 = "RFB 003.007\n", + versionMsg_3_8 = "RFB 003.008\n"; + + // Vendor signatures: standard VNC/RealVNC, TridiaVNC, and TightVNC + public final static String + StandardVendor = "STDV", + TridiaVncVendor = "TRDV", + TightVncVendor = "TGHT"; + + // Security types + public final static int + SecTypeInvalid = 0, + SecTypeNone = 1, + SecTypeVncAuth = 2, + SecTypeTight = 16; + + // Supported tunneling types + public final static int + NoTunneling = 0; + public final static String + SigNoTunneling = "NOTUNNEL"; + + // Supported authentication types + public final static int + AuthNone = 1, + AuthVNC = 2, + AuthUnixLogin = 129; + public final static String + SigAuthNone = "NOAUTH__", + SigAuthVNC = "VNCAUTH_", + SigAuthUnixLogin = "ULGNAUTH"; + + // VNC authentication results + public final static int + VncAuthOK = 0, + VncAuthFailed = 1, + VncAuthTooMany = 2; + + // Server-to-client messages + public final static int + FramebufferUpdate = 0, + SetColourMapEntries = 1, + Bell = 2, + ServerCutText = 3; + + // Client-to-server messages + public final static int + SetPixelFormat = 0, + FixColourMapEntries = 1, + SetEncodings = 2, + FramebufferUpdateRequest = 3, + KeyboardEvent = 4, + PointerEvent = 5, + ClientCutText = 6, + VMOpsClientCustom = 250; + + // Supported encodings and pseudo-encodings + public final static int + EncodingRaw = 0, + EncodingCopyRect = 1, + EncodingRRE = 2, + EncodingCoRRE = 4, + EncodingHextile = 5, + EncodingZlib = 6, + EncodingTight = 7, + EncodingZRLE = 16, + EncodingCompressLevel0 = 0xFFFFFF00, + EncodingQualityLevel0 = 0xFFFFFFE0, + EncodingXCursor = 0xFFFFFF10, + EncodingRichCursor = 0xFFFFFF11, + EncodingPointerPos = 0xFFFFFF18, + EncodingLastRect = 0xFFFFFF20, + EncodingNewFBSize = 0xFFFFFF21; + public final static String + SigEncodingRaw = "RAW_____", + SigEncodingCopyRect = "COPYRECT", + SigEncodingRRE = "RRE_____", + SigEncodingCoRRE = "CORRE___", + SigEncodingHextile = "HEXTILE_", + SigEncodingZlib = "ZLIB____", + SigEncodingTight = "TIGHT___", + SigEncodingZRLE = "ZRLE____", + SigEncodingCompressLevel0 = "COMPRLVL", + SigEncodingQualityLevel0 = "JPEGQLVL", + SigEncodingXCursor = "X11CURSR", + SigEncodingRichCursor = "RCHCURSR", + SigEncodingPointerPos = "POINTPOS", + SigEncodingLastRect = "LASTRECT", + SigEncodingNewFBSize = "NEWFBSIZ"; + + public final static int MaxNormalEncoding = 255; + + // Contstants used in the Hextile decoder + public final static int + HextileRaw = 1, + HextileBackgroundSpecified = 2, + HextileForegroundSpecified = 4, + HextileAnySubrects = 8, + HextileSubrectsColoured = 16; + + // Contstants used in the Tight decoder + public final static int TightMinToCompress = 12; + public final static int + TightExplicitFilter = 0x04, + TightFill = 0x08, + TightJpeg = 0x09, + TightMaxSubencoding = 0x09, + TightFilterCopy = 0x00, + TightFilterPalette = 0x01, + TightFilterGradient = 0x02; + + + String host; + int port; + Socket sock; + public SplitInputStream sis; + public DataInputStream is; + public OutputStream os; + + SessionRecorder rec; + public boolean inNormalProtocol = false; + + RfbProtoAdapter adapter; + // ConsoleViewer viewer; + + // Java on UNIX does not call keyPressed() on some keys, for example + // swedish keys To prevent our workaround to produce duplicate + // keypresses on JVMs that actually works, keep track of if + // keyPressed() for a "broken" key was called or not. + boolean brokenKeyPressed = false; + + // This will be set to true on the first framebuffer update + // containing Zlib-, ZRLE- or Tight-encoded data. + boolean wereZlibUpdates = false; + + // This will be set to false if the startSession() was called after + // we have received at least one Zlib-, ZRLE- or Tight-encoded + // framebuffer update. + boolean recordFromBeginning = true; + + // This fields are needed to show warnings about inefficiently saved + // sessions only once per each saved session file. + boolean zlibWarningShown; + boolean tightWarningShown; + + // Before starting to record each saved session, we set this field + // to 0, and increment on each framebuffer update. We don't flush + // the SessionRecorder data into the file before the second update. + // This allows us to write initial framebuffer update with zero + // timestamp, to let the player show initial desktop before + // playback. + int numUpdatesInSession; + + // Measuring network throughput. + boolean timing; + long timeWaitedIn100us; + long timedKbits; + + // Protocol version and TightVNC-specific protocol options. + public int serverMajor, serverMinor; + public int clientMajor, clientMinor; + boolean protocolTightVNC; + CapsContainer tunnelCaps, authCaps; + CapsContainer serverMsgCaps, clientMsgCaps; + CapsContainer encodingCaps; + + // If true, informs that the RFB socket was closed. + private boolean closed; + + // + // Constructor. Make TCP connection to RFB server. + // + + // RfbProto(String h, int p, ConsoleViewer v) throws IOException { + public RfbProto(String h, int p, RfbProtoAdapter adapter) throws IOException { + this.adapter = adapter; + host = h; + port = p; + +/* + if (!viewer.inProxyMode) { +* + // Create a trust manager that does not validate certificate chains like the default TrustManager + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { + //No need to implement. + } + + public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { + //No need to implement. + } + } + }; + + // Let us create the factory where we can set some parameters for the connection + SSLContext sc = null; + + try { + sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + } catch (Exception e) { + throw new IOException(e); + } + + SSLSocketFactory socketFactory =sc.getSocketFactory(); +* + + SocketFactory socketFactory = SSLSocketFactory.getDefault(); + sock = socketFactory.createSocket(host, port); + } else { + sock = new Socket(); + sock.setSoTimeout(0); // infinite + sock.setKeepAlive(true); + sock.connect(new InetSocketAddress(host, port), 30000); + } +*/ + sock = adapter.createConnection(h, p); + + sis = new SplitInputStream(new BufferedInputStream(sock.getInputStream(), 16384)); + is = new DataInputStream(sis); + os = sock.getOutputStream(); + + timing = false; + timeWaitedIn100us = 5; + timedKbits = 0; + } + + public synchronized void close() { + if (sock != null && !closed) { + try { + if(s_logger.isInfoEnabled()) + s_logger.info("close RFB socket"); + + // set linger to zero and let us quit quickly + sock.setSoLinger(true, 0); + sock.close(); + + sock = null; + closed = true; + if (rec != null) { + rec.close(); + rec = null; + } + } catch (Exception e) { + if(s_logger.isInfoEnabled()) + s_logger.info("Exception in closing RFB socket, " + e.toString()); + + sock = null; + closed = true; + + if (rec != null) { + try { + rec.close(); + } catch (IOException e1) { + } + rec = null; + } + } + + if(s_logger.isInfoEnabled()) + s_logger.info("RFB socket is closed"); + } + } + + public synchronized boolean closed() { + return closed; + } + + // + // Read proxy's protocol version message + // + + public void readProxyVersion() throws Exception { + byte[] version = new byte[12]; + readFully(version); + if (version[0] != 'R' || version[1] != 'F' || version[2] != 'B') { + throw new Exception("Bad version string from proxy"); + } + } + + // + // Write host:port to proxy + // + + public void writeProxyString(String realhost, int realport, String sid) throws IOException { + byte[] bs = (realhost + ":" + realport + ":" + sid + "\n").getBytes("US-ASCII"); +//s_logger.info("writeProxyString " + realhost + ":" + realport ); + os.write('V'); + os.write('M'); + os.write(bs.length); + os.write(bs); + } + + // + // Read server's protocol version message + // + + public void readVersionMsg() throws Exception { + + byte[] b = new byte[12]; + + readFully(b); + + if ((b[0] != 'R') || (b[1] != 'F') || (b[2] != 'B') || (b[3] != ' ') + || (b[4] < '0') || (b[4] > '9') || (b[5] < '0') || (b[5] > '9') + || (b[6] < '0') || (b[6] > '9') || (b[7] != '.') + || (b[8] < '0') || (b[8] > '9') || (b[9] < '0') || (b[9] > '9') + || (b[10] < '0') || (b[10] > '9') || (b[11] != '\n')) + { + throw new Exception("Host " + host + " port " + port + + " is not an RFB server"); + } + + serverMajor = (b[4] - '0') * 100 + (b[5] - '0') * 10 + (b[6] - '0'); + serverMinor = (b[8] - '0') * 100 + (b[9] - '0') * 10 + (b[10] - '0'); + + if (serverMajor < 3) { + throw new Exception("RFB server does not support protocol version 3"); + } + } + + + // + // Write our protocol version message + // + + public void writeVersionMsg() throws IOException { + clientMajor = 3; + if (serverMajor > 3 || serverMinor >= 8) { + clientMinor = 8; + os.write(versionMsg_3_8.getBytes()); + } else if (serverMinor >= 7) { + clientMinor = 7; + os.write(versionMsg_3_7.getBytes()); + } else { + clientMinor = 3; + os.write(versionMsg_3_3.getBytes()); + } + protocolTightVNC = false; + } + + + // + // Negotiate the authentication scheme. + // + + public int negotiateSecurity() throws Exception { + return (clientMinor >= 7) ? + selectSecurityType() : readSecurityType(); + } + + // + // Read security type from the server (protocol version 3.3). + // + + public int readSecurityType() throws Exception { + int secType = is.readInt(); + + switch (secType) { + case SecTypeInvalid: + readConnFailedReason(); + return SecTypeInvalid; // should never be executed + case SecTypeNone: + case SecTypeVncAuth: + return secType; + default: + throw new Exception("Unknown security type from RFB server: " + secType); + } + } + + // + // Select security type from the server's list (protocol versions 3.7/3.8). + // + + public int selectSecurityType() throws Exception { + int secType = SecTypeInvalid; + + // Read the list of secutiry types. + int nSecTypes = is.readUnsignedByte(); + if (nSecTypes == 0) { + readConnFailedReason(); + return SecTypeInvalid; // should never be executed + } + byte[] secTypes = new byte[nSecTypes]; + readFully(secTypes); + + // Find out if the server supports TightVNC protocol extensions + for (int i = 0; i < nSecTypes; i++) { + if (secTypes[i] == SecTypeTight) { + protocolTightVNC = true; + os.write(SecTypeTight); + return SecTypeTight; + } + } + + // Find first supported security type. + for (int i = 0; i < nSecTypes; i++) { + if (secTypes[i] == SecTypeNone || secTypes[i] == SecTypeVncAuth) { + secType = secTypes[i]; + break; + } + } + + if (secType == SecTypeInvalid) { + throw new Exception("Server did not offer supported security type"); + } else { + os.write(secType); + } + + return secType; + } + + // + // Perform "no authentication". + // + + public void authenticateNone() throws Exception { + if (clientMinor >= 8) + readSecurityResult("No authentication"); + } + + // + // Perform standard VNC Authentication. + // + + public void authenticateVNC(String pw) throws Exception { + byte[] challenge = new byte[16]; + readFully(challenge); + + if (pw.length() > 8) + pw = pw.substring(0, 8); // Truncate to 8 chars + + // Truncate password on the first zero byte. + int firstZero = pw.indexOf(0); + if (firstZero != -1) + pw = pw.substring(0, firstZero); + + byte[] key = {0, 0, 0, 0, 0, 0, 0, 0}; + System.arraycopy(pw.getBytes(), 0, key, 0, pw.length()); + + DesCipher des = new DesCipher(key); + + des.encrypt(challenge, 0, challenge, 0); + des.encrypt(challenge, 8, challenge, 8); + + os.write(challenge); + + readSecurityResult("VNC authentication"); + } + + // + // Read security result. + // Throws an exception on authentication failure. + // + + public void readSecurityResult(String authType) throws Exception { + int securityResult = is.readInt(); + + switch (securityResult) { + case VncAuthOK: +// s_logger.info(authType + ": success"); + break; + case VncAuthFailed: + if (clientMinor >= 8) + readConnFailedReason(); + throw new AuthenticationException(authType + ": failed"); + case VncAuthTooMany: + throw new AuthenticationException(authType + ": failed, too many tries"); + default: + throw new AuthenticationException(authType + ": unknown result " + securityResult); + } + } + + // + // Read the string describing the reason for a connection failure, + // and throw an exception. + // + + public void readConnFailedReason() throws Exception { + int reasonLen = is.readInt(); + byte[] reason = new byte[reasonLen]; + readFully(reason); + throw new AuthenticationException(new String(reason)); + } + + // + // Initialize capability lists (TightVNC protocol extensions). + // + + public void initCapabilities() { + tunnelCaps = new CapsContainer(); + authCaps = new CapsContainer(); + serverMsgCaps = new CapsContainer(); + clientMsgCaps = new CapsContainer(); + encodingCaps = new CapsContainer(); + + // Supported authentication methods + authCaps.add(AuthNone, StandardVendor, SigAuthNone, + "No authentication"); + authCaps.add(AuthVNC, StandardVendor, SigAuthVNC, + "Standard VNC password authentication"); + + // Supported encoding types + encodingCaps.add(EncodingCopyRect, StandardVendor, + SigEncodingCopyRect, "Standard CopyRect encoding"); + encodingCaps.add(EncodingRRE, StandardVendor, + SigEncodingRRE, "Standard RRE encoding"); + encodingCaps.add(EncodingCoRRE, StandardVendor, + SigEncodingCoRRE, "Standard CoRRE encoding"); + encodingCaps.add(EncodingHextile, StandardVendor, + SigEncodingHextile, "Standard Hextile encoding"); + encodingCaps.add(EncodingZRLE, StandardVendor, + SigEncodingZRLE, "Standard ZRLE encoding"); + encodingCaps.add(EncodingZlib, TridiaVncVendor, + SigEncodingZlib, "Zlib encoding"); + encodingCaps.add(EncodingTight, TightVncVendor, + SigEncodingTight, "Tight encoding"); + + // Supported pseudo-encoding types + encodingCaps.add(EncodingCompressLevel0, TightVncVendor, + SigEncodingCompressLevel0, "Compression level"); + encodingCaps.add(EncodingQualityLevel0, TightVncVendor, + SigEncodingQualityLevel0, "JPEG quality level"); + encodingCaps.add(EncodingXCursor, TightVncVendor, + SigEncodingXCursor, "X-style cursor shape update"); + encodingCaps.add(EncodingRichCursor, TightVncVendor, + SigEncodingRichCursor, "Rich-color cursor shape update"); + encodingCaps.add(EncodingPointerPos, TightVncVendor, + SigEncodingPointerPos, "Pointer position update"); + encodingCaps.add(EncodingLastRect, TightVncVendor, + SigEncodingLastRect, "LastRect protocol extension"); + encodingCaps.add(EncodingNewFBSize, TightVncVendor, + SigEncodingNewFBSize, "Framebuffer size change"); + } + + // + // Setup tunneling (TightVNC protocol extensions) + // + + public void setupTunneling() throws IOException { + int nTunnelTypes = is.readInt(); + if (nTunnelTypes != 0) { + readCapabilityList(tunnelCaps, nTunnelTypes); + + // We don't support tunneling yet. + writeInt(NoTunneling); + } + } + + // + // Negotiate authentication scheme (TightVNC protocol extensions) + // + + public int negotiateAuthenticationTight() throws Exception { + int nAuthTypes = is.readInt(); + if (nAuthTypes == 0) + return AuthNone; + + readCapabilityList(authCaps, nAuthTypes); + for (int i = 0; i < authCaps.numEnabled(); i++) { + int authType = authCaps.getByOrder(i); + if (authType == AuthNone || authType == AuthVNC) { + writeInt(authType); + return authType; + } + } + throw new Exception("No suitable authentication scheme found"); + } + + // + // Read a capability list (TightVNC protocol extensions) + // + + public void readCapabilityList(CapsContainer caps, int count) throws IOException { + int code; + byte[] vendor = new byte[4]; + byte[] name = new byte[8]; + for (int i = 0; i < count; i++) { + code = is.readInt(); + readFully(vendor); + readFully(name); + caps.enable(new CapabilityInfo(code, vendor, name)); + } + } + + // + // Write a 32-bit integer into the output stream. + // + + public void writeInt(int value) throws IOException { + byte[] b = new byte[4]; + b[0] = (byte) ((value >> 24) & 0xff); + b[1] = (byte) ((value >> 16) & 0xff); + b[2] = (byte) ((value >> 8) & 0xff); + b[3] = (byte) (value & 0xff); + os.write(b); + } + + // + // Write the client initialisation message + // + + public void writeClientInit() throws IOException { +/* + if (viewer.options.shareDesktop) { + os.write(1); + } else { + os.write(0); + } +*/ + adapter.writeInit(os); +// viewer.options.disableShareDesktop(); + } + + + // + // Read the server initialisation message + // + + public String desktopName; + public int framebufferWidth, framebufferHeight; + public int bitsPerPixel, depth; + public boolean bigEndian, trueColour; + public int redMax, greenMax, blueMax, redShift, greenShift, blueShift; + + public void readServerInit() throws IOException { + framebufferWidth = is.readUnsignedShort(); + framebufferHeight = is.readUnsignedShort(); + bitsPerPixel = is.readUnsignedByte(); + depth = is.readUnsignedByte(); + bigEndian = (is.readUnsignedByte() != 0); + trueColour = (is.readUnsignedByte() != 0); + redMax = is.readUnsignedShort(); + greenMax = is.readUnsignedShort(); + blueMax = is.readUnsignedShort(); + redShift = is.readUnsignedByte(); + greenShift = is.readUnsignedByte(); + blueShift = is.readUnsignedByte(); + byte[] pad = new byte[3]; + readFully(pad); + int nameLength = is.readInt(); + byte[] name = new byte[nameLength]; + readFully(name); + desktopName = new String(name); + + // Read interaction capabilities (TightVNC protocol extensions) + if (protocolTightVNC) { + int nServerMessageTypes = is.readUnsignedShort(); + int nClientMessageTypes = is.readUnsignedShort(); + int nEncodingTypes = is.readUnsignedShort(); + is.readUnsignedShort(); + readCapabilityList(serverMsgCaps, nServerMessageTypes); + readCapabilityList(clientMsgCaps, nClientMessageTypes); + readCapabilityList(encodingCaps, nEncodingTypes); + } + + if(s_logger.isInfoEnabled()) + s_logger.info("Recv'd ServerInit w=" + framebufferWidth + + " h=" + framebufferHeight + + " bits=" + bitsPerPixel + + " depth=" + depth + + " name=" + desktopName); + + inNormalProtocol = true; + } + + + // + // Create session file and write initial protocol messages into it. + // + + public void startSession(String fname) throws IOException { + rec = new SessionRecorder(fname); + rec.writeHeader(); + rec.write(versionMsg_3_3.getBytes()); + rec.writeIntBE(SecTypeNone); + rec.writeShortBE(framebufferWidth); + rec.writeShortBE(framebufferHeight); + byte[] fbsServerInitMsg = { + 32, 24, 0, 1, 0, + (byte)0xFF, 0, (byte)0xFF, 0, (byte)0xFF, + 16, 8, 0, 0, 0, 0 + }; + rec.write(fbsServerInitMsg); + rec.writeIntBE(desktopName.length()); + rec.write(desktopName.getBytes()); + numUpdatesInSession = 0; + + // FIXME: If there were e.g. ZRLE updates only, that should not + // affect recording of Zlib and Tight updates. So, actually + // we should maintain separate flags for Zlib, ZRLE and + // Tight, instead of one ``wereZlibUpdates'' variable. + // + if (wereZlibUpdates) + recordFromBeginning = false; + + zlibWarningShown = false; + tightWarningShown = false; + } + + // + // Close session file. + // + + public void closeSession() throws IOException { + if (rec != null) { + rec.close(); + rec = null; + } + } + + + // + // Set new framebuffer size + // + + public void setFramebufferSize(int width, int height) { + framebufferWidth = width; + framebufferHeight = height; + } + + + // + // Read the server message type + // + + public int readServerMessageType() throws IOException { + int msgType = is.readUnsignedByte(); + + // If the session is being recorded: + if (rec != null) { + if (msgType == Bell) { // Save Bell messages in session files. + rec.writeByte(msgType); + if (numUpdatesInSession > 0) + rec.flush(); + } + } + + return msgType; + } + + + // + // Read a FramebufferUpdate message + // + + int updateNRects; + + public void readFramebufferUpdate() throws IOException { + is.readByte(); + updateNRects = is.readUnsignedShort(); + + // If the session is being recorded: + if (rec != null) { + rec.writeByte(FramebufferUpdate); + rec.writeByte(0); + rec.writeShortBE(updateNRects); + } + + numUpdatesInSession++; + } + + // Read a FramebufferUpdate rectangle header + + int updateRectX, updateRectY, updateRectW, updateRectH, updateRectEncoding; + + public void readFramebufferUpdateRectHdr() throws Exception { + updateRectX = is.readUnsignedShort(); + updateRectY = is.readUnsignedShort(); + updateRectW = is.readUnsignedShort(); + updateRectH = is.readUnsignedShort(); + updateRectEncoding = is.readInt(); + + if (updateRectEncoding == EncodingZlib || + updateRectEncoding == EncodingZRLE || + updateRectEncoding == EncodingTight) + wereZlibUpdates = true; + + // If the session is being recorded: + if (rec != null) { + if (numUpdatesInSession > 1) + rec.flush(); // Flush the output on each rectangle. + rec.writeShortBE(updateRectX); + rec.writeShortBE(updateRectY); + rec.writeShortBE(updateRectW); + rec.writeShortBE(updateRectH); + if (updateRectEncoding == EncodingZlib && !recordFromBeginning) { + // Here we cannot write Zlib-encoded rectangles because the + // decoder won't be able to reproduce zlib stream state. + if (!zlibWarningShown) { + if(s_logger.isInfoEnabled()) + s_logger.info("Warning: Raw encoding will be used " + "instead of Zlib in recorded session."); + zlibWarningShown = true; + } + rec.writeIntBE(EncodingRaw); + } else { + rec.writeIntBE(updateRectEncoding); + if (updateRectEncoding == EncodingTight && !recordFromBeginning && + !tightWarningShown) { + if(s_logger.isInfoEnabled()) + s_logger.info("Warning: Re-compressing Tight-encoded " + + "updates for session recording."); + tightWarningShown = true; + } + } + } + + if (updateRectEncoding < 0 || updateRectEncoding > MaxNormalEncoding) + return; + + if (updateRectX + updateRectW > framebufferWidth || + updateRectY + updateRectH > framebufferHeight) { + s_logger.warn("Framebuffer update rectangle too large: " + + updateRectW + "x" + updateRectH + " at (" + + updateRectX + "," + updateRectY + "), current framebuffer size:" + + framebufferWidth + ", " + framebufferHeight); + + throw new Exception("Framebuffer update rectangle too large: " + + updateRectW + "x" + updateRectH + " at (" + + updateRectX + "," + updateRectY + "), current framebuffer size:" + + framebufferWidth + ", " + framebufferHeight); + } + } + + // Read CopyRect source X and Y. + + int copyRectSrcX, copyRectSrcY; + + public void readCopyRect() throws IOException { + copyRectSrcX = is.readUnsignedShort(); + copyRectSrcY = is.readUnsignedShort(); + + // If the session is being recorded: + if (rec != null) { + rec.writeShortBE(copyRectSrcX); + rec.writeShortBE(copyRectSrcY); + } + } + + + // + // Read a ServerCutText message + // + + public String readServerCutText() throws IOException { + byte[] pad = new byte[3]; + readFully(pad); + int len = is.readInt(); + byte[] text = new byte[len]; + readFully(text); + return new String(text); + } + + + // + // Read an integer in compact representation (1..3 bytes). + // Such format is used as a part of the Tight encoding. + // Also, this method records data if session recording is active and + // the viewer's recordFromBeginning variable is set to true. + // + + public int readCompactLen() throws IOException { + int[] portion = new int[3]; + portion[0] = is.readUnsignedByte(); + int byteCount = 1; + int len = portion[0] & 0x7F; + if ((portion[0] & 0x80) != 0) { + portion[1] = is.readUnsignedByte(); + byteCount++; + len |= (portion[1] & 0x7F) << 7; + if ((portion[1] & 0x80) != 0) { + portion[2] = is.readUnsignedByte(); + byteCount++; + len |= (portion[2] & 0xFF) << 14; + } + } + + if (rec != null && recordFromBeginning) + for (int i = 0; i < byteCount; i++) + rec.writeByte(portion[i]); + + return len; + } + + // + // Write a VMOps Client Custom message + // + + public void writeClientCustomMessage(int type) + throws IOException + { + byte[] b = new byte[2]; + + b[0] = (byte) VMOpsClientCustom; + b[1] = (byte) type; + + os.write(b); + } + + // + // Write a FramebufferUpdateRequest message + // + + public void writeFramebufferUpdateRequest(int x, int y, int w, int h, + boolean incremental) + throws IOException + { + byte[] b = new byte[10]; + + b[0] = (byte) FramebufferUpdateRequest; + b[1] = (byte) (incremental ? 1 : 0); + b[2] = (byte) ((x >> 8) & 0xff); + b[3] = (byte) (x & 0xff); + b[4] = (byte) ((y >> 8) & 0xff); + b[5] = (byte) (y & 0xff); + b[6] = (byte) ((w >> 8) & 0xff); + b[7] = (byte) (w & 0xff); + b[8] = (byte) ((h >> 8) & 0xff); + b[9] = (byte) (h & 0xff); + + os.write(b); + } + + + // + // Write a SetPixelFormat message + // + + public void writeSetPixelFormat(int bitsPerPixel, int depth, boolean bigEndian, + boolean trueColour, + int redMax, int greenMax, int blueMax, + int redShift, int greenShift, int blueShift) + throws IOException + { + byte[] b = new byte[20]; + + b[0] = (byte) SetPixelFormat; + b[4] = (byte) bitsPerPixel; + b[5] = (byte) depth; + b[6] = (byte) (bigEndian ? 1 : 0); + b[7] = (byte) (trueColour ? 1 : 0); + b[8] = (byte) ((redMax >> 8) & 0xff); + b[9] = (byte) (redMax & 0xff); + b[10] = (byte) ((greenMax >> 8) & 0xff); + b[11] = (byte) (greenMax & 0xff); + b[12] = (byte) ((blueMax >> 8) & 0xff); + b[13] = (byte) (blueMax & 0xff); + b[14] = (byte) redShift; + b[15] = (byte) greenShift; + b[16] = (byte) blueShift; + + os.write(b); + } + + + // + // Write a FixColourMapEntries message. The values in the red, green and + // blue arrays are from 0 to 65535. + // + + public void writeFixColourMapEntries(int firstColour, int nColours, + int[] red, int[] green, int[] blue) + throws IOException + { + byte[] b = new byte[6 + nColours * 6]; + + b[0] = (byte) FixColourMapEntries; + b[2] = (byte) ((firstColour >> 8) & 0xff); + b[3] = (byte) (firstColour & 0xff); + b[4] = (byte) ((nColours >> 8) & 0xff); + b[5] = (byte) (nColours & 0xff); + + for (int i = 0; i < nColours; i++) { + b[6 + i * 6] = (byte) ((red[i] >> 8) & 0xff); + b[6 + i * 6 + 1] = (byte) (red[i] & 0xff); + b[6 + i * 6 + 2] = (byte) ((green[i] >> 8) & 0xff); + b[6 + i * 6 + 3] = (byte) (green[i] & 0xff); + b[6 + i * 6 + 4] = (byte) ((blue[i] >> 8) & 0xff); + b[6 + i * 6 + 5] = (byte) (blue[i] & 0xff); + } + + os.write(b); + } + + + // + // Write a SetEncodings message + // + + public void writeSetEncodings(int[] encs, int len) throws IOException { + byte[] b = new byte[4 + 4 * len]; + + b[0] = (byte) SetEncodings; + b[2] = (byte) ((len >> 8) & 0xff); + b[3] = (byte) (len & 0xff); + + for (int i = 0; i < len; i++) { + b[4 + 4 * i] = (byte) ((encs[i] >> 24) & 0xff); + b[5 + 4 * i] = (byte) ((encs[i] >> 16) & 0xff); + b[6 + 4 * i] = (byte) ((encs[i] >> 8) & 0xff); + b[7 + 4 * i] = (byte) (encs[i] & 0xff); + } + + os.write(b); + } + + + // + // Write a ClientCutText message + // + + public void writeClientCutText(String text) throws IOException { + byte[] b = new byte[8 + text.length()]; + + b[0] = (byte) ClientCutText; + b[4] = (byte) ((text.length() >> 24) & 0xff); + b[5] = (byte) ((text.length() >> 16) & 0xff); + b[6] = (byte) ((text.length() >> 8) & 0xff); + b[7] = (byte) (text.length() & 0xff); + + System.arraycopy(text.getBytes(), 0, b, 8, text.length()); + + os.write(b); + } + + + // + // A buffer for putting pointer and keyboard events before being sent. This + // is to ensure that multiple RFB events generated from a single Java Event + // will all be sent in a single network packet. The maximum possible + // length is 4 modifier down events, a single key event followed by 4 + // modifier up events i.e. 9 key events or 72 bytes. + // + + byte[] eventBuf = new byte[72]; + int eventBufLen; + + + // Useful shortcuts for modifier masks. + + final static int CTRL_MASK = InputEvent.CTRL_MASK; + final static int SHIFT_MASK = InputEvent.SHIFT_MASK; + final static int META_MASK = InputEvent.META_MASK; + final static int ALT_MASK = InputEvent.ALT_MASK; + + + // + // Write a pointer event message. We may need to send modifier key events + // around it to set the correct modifier state. + // + + int pointerMask = 0; + + public void writePointerEvent(MouseEvent evt) throws IOException { + int modifiers = evt.getModifiers(); + + int mask2 = 2; + int mask3 = 4; + Integer[] masks = new Integer[] { new Integer(mask2), new Integer(mask3) }; + adapter.swapMouseButton(masks); + mask2 = masks[0]; + mask3 = masks[1]; + + /* + if (viewer.options.reverseMouseButtons2And3) { + mask2 = 4; + mask3 = 2; + } + */ + + // Note: For some reason, AWT does not set BUTTON1_MASK on left + // button presses. Here we think that it was the left button if + // modifiers do not include BUTTON2_MASK or BUTTON3_MASK. + + if (evt.getID() == MouseEvent.MOUSE_PRESSED) { + if ((modifiers & InputEvent.BUTTON2_MASK) != 0) { + pointerMask = mask2; + modifiers &= ~ALT_MASK; + } else if ((modifiers & InputEvent.BUTTON3_MASK) != 0) { + pointerMask = mask3; + modifiers &= ~META_MASK; + } else { + pointerMask = 1; + } + } else if (evt.getID() == MouseEvent.MOUSE_RELEASED) { + pointerMask = 0; + if ((modifiers & InputEvent.BUTTON2_MASK) != 0) { + modifiers &= ~ALT_MASK; + } else if ((modifiers & InputEvent.BUTTON3_MASK) != 0) { + modifiers &= ~META_MASK; + } + } + + eventBufLen = 0; + writeModifierKeyEvents(modifiers); + + int x = evt.getX(); + int y = evt.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + eventBuf[eventBufLen++] = (byte) PointerEvent; + eventBuf[eventBufLen++] = (byte) pointerMask; + eventBuf[eventBufLen++] = (byte) ((x >> 8) & 0xff); + eventBuf[eventBufLen++] = (byte) (x & 0xff); + eventBuf[eventBufLen++] = (byte) ((y >> 8) & 0xff); + eventBuf[eventBufLen++] = (byte) (y & 0xff); + + // + // Always release all modifiers after an "up" event + // + + if (pointerMask == 0) { + writeModifierKeyEvents(0); + } + + os.write(eventBuf, 0, eventBufLen); + } + + + // + // Write a key event message. We may need to send modifier key events + // around it to set the correct modifier state. Also we need to translate + // from the Java key values to the X keysym values used by the RFB protocol. + // + public void writeKeyEvent(KeyEvent evt) throws IOException { + + int keyChar = evt.getKeyChar(); + + // + // Ignore event if only modifiers were pressed. + // + + // Some JVMs return 0 instead of CHAR_UNDEFINED in getKeyChar(). + if (keyChar == 0) + keyChar = KeyEvent.CHAR_UNDEFINED; + + if (keyChar == KeyEvent.CHAR_UNDEFINED) { + int code = evt.getKeyCode(); + if (code == KeyEvent.VK_CONTROL || code == KeyEvent.VK_SHIFT || + code == KeyEvent.VK_META || code == KeyEvent.VK_ALT) + + if(s_logger.isInfoEnabled()) + s_logger.info("Ignore sending of modifier key"); + return; + } + + // + // Key press or key release? + // + + boolean down = (evt.getID() == KeyEvent.KEY_PRESSED); + + int key; + if (evt.isActionKey()) { + + // + // An action key should be one of the following. + // If not then just ignore the event. + // + + switch(evt.getKeyCode()) { + case KeyEvent.VK_HOME: key = 0xff50; break; + case KeyEvent.VK_LEFT: key = 0xff51; break; + case KeyEvent.VK_UP: key = 0xff52; break; + case KeyEvent.VK_RIGHT: key = 0xff53; break; + case KeyEvent.VK_DOWN: key = 0xff54; break; + case KeyEvent.VK_PAGE_UP: key = 0xff55; break; + case KeyEvent.VK_PAGE_DOWN: key = 0xff56; break; + case KeyEvent.VK_END: key = 0xff57; break; + case KeyEvent.VK_INSERT: key = 0xff63; break; + case KeyEvent.VK_F1: key = 0xffbe; break; + case KeyEvent.VK_F2: key = 0xffbf; break; + case KeyEvent.VK_F3: key = 0xffc0; break; + case KeyEvent.VK_F4: key = 0xffc1; break; + case KeyEvent.VK_F5: key = 0xffc2; break; + case KeyEvent.VK_F6: key = 0xffc3; break; + case KeyEvent.VK_F7: key = 0xffc4; break; + case KeyEvent.VK_F8: key = 0xffc5; break; + case KeyEvent.VK_F9: key = 0xffc6; break; + case KeyEvent.VK_F10: key = 0xffc7; break; + case KeyEvent.VK_F11: key = 0xffc8; break; + case KeyEvent.VK_F12: key = 0xffc9; break; + + default: + if(s_logger.isInfoEnabled()) + s_logger.info("Ignore sending of un-supprted action key, key code: " + evt.getKeyCode()); + return; + } + + } else { + + // + // A "normal" key press. Ordinary ASCII characters go straight through. + // For CTRL-, CTRL is sent separately so just send . + // Backspace, tab, return, escape and delete have special keysyms. + // Anything else we ignore. + // + + key = keyChar; + + if (key < 0x20) { + boolean converted = false; + switch(key) { + case KeyEvent.VK_BACK_SPACE: key = 0xff08; converted = true; break; + case KeyEvent.VK_TAB: key = 0xff09; converted = true; break; + case KeyEvent.VK_ENTER: key = 0xff0d; converted = true; break; + case KeyEvent.VK_ESCAPE: key = 0xff1b; converted = true; break; + + // For KVM QEMU + case KeyEvent.VK_SHIFT: key = 0xffe1; converted = true; break; + } + + if(evt.isControlDown() && !converted) { + if(key != KeyEvent.VK_CONTROL) + key += 0x60; + } + + /* + if (evt.isControlDown()) { + if(key != KeyEvent.VK_CONTROL && key != KeyEvent.VK_ESCAPE) + key += 0x60; + else if(key == KeyEvent.VK_ESCAPE) + key = 0xff1b; + } else { + switch(key) { + case KeyEvent.VK_BACK_SPACE: key = 0xff08; break; + case KeyEvent.VK_TAB: key = 0xff09; break; + case KeyEvent.VK_ENTER: key = 0xff0d; break; + case KeyEvent.VK_ESCAPE: key = 0xff1b; break; + } + } + */ + + } else if (key == 0x7f) { + // Delete + key = 0xffff; + } else if (key > 0xff) { + // JDK1.1 on X incorrectly passes some keysyms straight through, + // so we do too. JDK1.1.4 seems to have fixed this. + // The keysyms passed are 0xff00 .. XK_BackSpace .. XK_Delete + // Also, we pass through foreign currency keysyms (0x20a0..0x20af). + if ((key < 0xff00 || key > 0xffff) && + !(key >= 0x20a0 && key <= 0x20af)) { + + if(s_logger.isInfoEnabled()) + s_logger.info("Ignore sending of un-supprted X keys, key : " + key); + return; + } + } + } + + // Fake keyPresses for keys that only generates keyRelease events + if ((key == 0xe5) || (key == 0xc5) || // XK_aring / XK_Aring + (key == 0xe4) || (key == 0xc4) || // XK_adiaeresis / XK_Adiaeresis + (key == 0xf6) || (key == 0xd6) || // XK_odiaeresis / XK_Odiaeresis + (key == 0xa7) || (key == 0xbd) || // XK_section / XK_onehalf + (key == 0xa3)) { // XK_sterling + // Make sure we do not send keypress events twice on platforms + // with correct JVMs (those that actually report KeyPress for all + // keys) + if (down) + brokenKeyPressed = true; + + if (!down && !brokenKeyPressed) { + // We've got a release event for this key, but haven't received + // a press. Fake it. + eventBufLen = 0; + writeModifierKeyEvents(evt.getModifiers()); + writeKeyEvent(key, true); + os.write(eventBuf, 0, eventBufLen); + } + + if (!down) + brokenKeyPressed = false; + } + + // special need for KVM QEMU + if(key == KeyEvent.VK_QUOTE) + key = 0x22; + + if(s_logger.isTraceEnabled()) + s_logger.trace("Write key event, key: " + key + ", down: " + down + ", modifiers: " + evt.getModifiers()); + + eventBufLen = 0; + writeModifierKeyEvents(evt.getModifiers()); + writeKeyEvent(key, down); + + // Always release all modifiers after an "up" event + if (!down) + writeModifierKeyEvents(0); + + os.write(eventBuf, 0, eventBufLen); + } + + + // + // Add a raw key event with the given X keysym to eventBuf. + // + + public void writeKeyEvent(int keysym, boolean down) { + eventBuf[eventBufLen++] = (byte) KeyboardEvent; + eventBuf[eventBufLen++] = (byte) (down ? 1 : 0); + eventBuf[eventBufLen++] = (byte) 0; + eventBuf[eventBufLen++] = (byte) 0; + eventBuf[eventBufLen++] = (byte) ((keysym >> 24) & 0xff); + eventBuf[eventBufLen++] = (byte) ((keysym >> 16) & 0xff); + eventBuf[eventBufLen++] = (byte) ((keysym >> 8) & 0xff); + eventBuf[eventBufLen++] = (byte) (keysym & 0xff); + + if(s_logger.isTraceEnabled()) + s_logger.trace("Send keyboard event on wire, keysym: 0x" + Integer.toHexString(keysym) + ", down: " + down); + } + + + // + // Write key events to set the correct modifier state. + // + int oldModifiers = 0; + + void writeModifierKeyEvents(int newModifiers) { + if ((newModifiers & CTRL_MASK) != (oldModifiers & CTRL_MASK)) + writeKeyEvent(0xffe3, (newModifiers & CTRL_MASK) != 0); + + if ((newModifiers & SHIFT_MASK) != (oldModifiers & SHIFT_MASK)) + writeKeyEvent(0xffe1, (newModifiers & SHIFT_MASK) != 0); + + if ((newModifiers & META_MASK) != (oldModifiers & META_MASK)) + writeKeyEvent(0xffe7, (newModifiers & META_MASK) != 0); + + if ((newModifiers & ALT_MASK) != (oldModifiers & ALT_MASK)) + writeKeyEvent(0xffe9, (newModifiers & ALT_MASK) != 0); + + oldModifiers = newModifiers; + } + + // + // Compress and write the data into the recorded session file. This + // method assumes the recording is on (rec != null). + // + public void recordCompressedData(byte[] data, int off, int len) throws IOException { + Deflater deflater = new Deflater(); + deflater.setInput(data, off, len); + int bufSize = len + len / 100 + 12; + byte[] buf = new byte[bufSize]; + deflater.finish(); + int compressedSize = deflater.deflate(buf); + recordCompactLen(compressedSize); + rec.write(buf, 0, compressedSize); + } + + public void recordCompressedData(byte[] data) throws IOException { + recordCompressedData(data, 0, data.length); + } + + // + // Write an integer in compact representation (1..3 bytes) into the + // recorded session file. This method assumes the recording is on + // (rec != null). + // + + public void recordCompactLen(int len) throws IOException { + byte[] buf = new byte[3]; + int bytes = 0; + buf[bytes++] = (byte)(len & 0x7F); + if (len > 0x7F) { + buf[bytes-1] |= 0x80; + buf[bytes++] = (byte)(len >> 7 & 0x7F); + if (len > 0x3FFF) { + buf[bytes-1] |= 0x80; + buf[bytes++] = (byte)(len >> 14 & 0xFF); + } + } + rec.write(buf, 0, bytes); + } + + public void startTiming() { + timing = true; + + // Carry over up to 1s worth of previous rate for smoothing. + + if (timeWaitedIn100us > 10000) { + timedKbits = timedKbits * 10000 / timeWaitedIn100us; + timeWaitedIn100us = 10000; + } + } + + public void stopTiming() { + timing = false; + if (timeWaitedIn100us < timedKbits/2) + timeWaitedIn100us = timedKbits/2; // upper limit 20Mbit/s + } + + public long kbitsPerSecond() { + return timedKbits * 10000 / timeWaitedIn100us; + } + + public long timeWaited() { + return timeWaitedIn100us; + } + + public void readFully(byte b[]) throws IOException { + readFully(b, 0, b.length); + } + + public void readFully(byte b[], int off, int len) throws IOException { + long before = 0; + if (timing) + before = System.currentTimeMillis(); + + is.readFully(b, off, len); + + if (timing) { + long after = System.currentTimeMillis(); + long newTimeWaited = (after - before) * 10; + int newKbits = len * 8 / 1000; + + // limit rate to between 10kbit/s and 40Mbit/s + + if (newTimeWaited > newKbits*1000) newTimeWaited = newKbits*1000; + if (newTimeWaited < newKbits/4) newTimeWaited = newKbits/4; + + timeWaitedIn100us += newTimeWaited; + timedKbits += newKbits; + } + } +} diff --git a/console/src/com/cloud/console/RfbProtoAdapter.java b/console/src/com/cloud/console/RfbProtoAdapter.java new file mode 100644 index 00000000000..06d581e1482 --- /dev/null +++ b/console/src/com/cloud/console/RfbProtoAdapter.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.console; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; + +public interface RfbProtoAdapter { + Socket createConnection(String host, int port) throws IOException; + void writeInit(OutputStream os) throws IOException; + void swapMouseButton(Integer[] masks); +} diff --git a/console/src/com/cloud/console/RfbViewer.java b/console/src/com/cloud/console/RfbViewer.java new file mode 100644 index 00000000000..6f88bf65117 --- /dev/null +++ b/console/src/com/cloud/console/RfbViewer.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.console; + +import java.awt.Dimension; +import java.io.IOException; + +public interface RfbViewer { + boolean isProxy(); + boolean hasClientConnection(); + + RfbProto getRfb(); + Dimension getScreenSize(); + Dimension getFrameSize(); + + int getScalingFactor(); + int getCursorScaleFactor(); + boolean ignoreCursorUpdate(); + int getDeferCursorUpdateTimeout(); + int getDeferScreenUpdateTimeout(); + int getDeferUpdateRequestTimeout(); + + int setPixelFormat(RfbProto rfb) throws IOException; + + void onInputEnabled(boolean enable); + + void onFramebufferSizeChange(int w, int h); + void onFramebufferUpdate(int x, int y, int w, int h); + void onFramebufferCursorMove(int x, int y); + void onFramebufferCursorShapeChange(int encodingType, + int xhot, int yhot, int width, int height, byte[] cursorData); + + void onDesktopResize(); + void onFrameResize(Dimension newSize); + void onDisconnectMessage(); + void onBellMessage(); + + void onPreProtocolProcess(byte[] bs) throws IOException; + boolean onPostFrameBufferUpdateProcess(boolean cursorPosReceived) throws IOException; + void onProtocolProcessException(IOException e); +} diff --git a/console/src/com/cloud/console/SessionRecorder.java b/console/src/com/cloud/console/SessionRecorder.java new file mode 100644 index 00000000000..fe49944ea8b --- /dev/null +++ b/console/src/com/cloud/console/SessionRecorder.java @@ -0,0 +1,196 @@ +// +// Copyright (C) 2002 Constantin Kaplinsky. All Rights Reserved. +// +// This is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this software; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +// USA. +// + +// +// SessionRecorder is a class to write FBS (FrameBuffer Stream) files. +// FBS files are used to save RFB sessions for later playback. +// + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +import java.io.*; + +public class SessionRecorder { + + protected FileOutputStream f; + protected DataOutputStream df; + protected long startTime, lastTimeOffset; + + protected byte[] buffer; + protected int bufferSize; + protected int bufferBytes; + + public SessionRecorder(String name, int bufsize) throws IOException { + f = new FileOutputStream(name); + df = new DataOutputStream(f); + startTime = System.currentTimeMillis(); + lastTimeOffset = 0; + + bufferSize = bufsize; + bufferBytes = 0; + buffer = new byte[bufferSize]; + } + + public SessionRecorder(String name) throws IOException { + this(name, 65536); + } + + // + // Close the file, free resources. + // + + public void close() throws IOException { + try { + flush(); + } catch (IOException e) { + } + + df = null; + f.close(); + f = null; + buffer = null; + } + + // + // Write the FBS file header as defined in the rfbproxy utility. + // + + public void writeHeader() throws IOException { + df.write("FBS 001.000\n".getBytes()); + } + + // + // Write one byte. + // + + public void writeByte(int b) throws IOException { + prepareWriting(); + buffer[bufferBytes++] = (byte)b; + } + + // + // Write 16-bit value, big-endian. + // + + public void writeShortBE(int v) throws IOException { + prepareWriting(); + buffer[bufferBytes++] = (byte)(v >> 8); + buffer[bufferBytes++] = (byte)v; + } + + // + // Write 32-bit value, big-endian. + // + + public void writeIntBE(int v) throws IOException { + prepareWriting(); + buffer[bufferBytes] = (byte)(v >> 24); + buffer[bufferBytes + 1] = (byte)(v >> 16); + buffer[bufferBytes + 2] = (byte)(v >> 8); + buffer[bufferBytes + 3] = (byte)v; + bufferBytes += 4; + } + + // + // Write 16-bit value, little-endian. + // + + public void writeShortLE(int v) throws IOException { + prepareWriting(); + buffer[bufferBytes++] = (byte)v; + buffer[bufferBytes++] = (byte)(v >> 8); + } + + // + // Write 32-bit value, little-endian. + // + + public void writeIntLE(int v) throws IOException { + prepareWriting(); + buffer[bufferBytes] = (byte)v; + buffer[bufferBytes + 1] = (byte)(v >> 8); + buffer[bufferBytes + 2] = (byte)(v >> 16); + buffer[bufferBytes + 3] = (byte)(v >> 24); + bufferBytes += 4; + } + + // + // Write byte arrays. + // + + public void write(byte b[], int off, int len) throws IOException { + prepareWriting(); + while (len > 0) { + if (bufferBytes > bufferSize - 4) + flush(false); + + int partLen; + if (bufferBytes + len > bufferSize) { + partLen = bufferSize - bufferBytes; + } else { + partLen = len; + } + System.arraycopy(b, off, buffer, bufferBytes, partLen); + bufferBytes += partLen; + off += partLen; + len -= partLen; + } + } + + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + // + // Flush the output. This method saves buffered data in the + // underlying file object adding data sizes and timestamps. If the + // updateTimeOffset is set to false, then the current time offset + // will not be changed for next write operation. + // + + public void flush(boolean updateTimeOffset) throws IOException { + if (bufferBytes > 0) { + df.writeInt(bufferBytes); + df.write(buffer, 0, (bufferBytes + 3) & 0x7FFFFFFC); + df.writeInt((int)lastTimeOffset); + bufferBytes = 0; + if (updateTimeOffset) + lastTimeOffset = -1; + } + } + + public void flush() throws IOException { + flush(true); + } + + // + // Before writing any data, remember time offset and flush the + // buffer before it becomes full. + // + + protected void prepareWriting() throws IOException { + if (lastTimeOffset == -1) + lastTimeOffset = System.currentTimeMillis() - startTime; + if (bufferBytes > bufferSize - 4) + flush(false); + } + +} + diff --git a/console/src/com/cloud/console/SplitInputStream.java b/console/src/com/cloud/console/SplitInputStream.java new file mode 100644 index 00000000000..4bb5bb95bf1 --- /dev/null +++ b/console/src/com/cloud/console/SplitInputStream.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +import java.io.*; + +public class SplitInputStream extends FilterInputStream { + ByteArrayOutputStream bo; + public SplitInputStream(InputStream in) { + super(in); + } + public int read() throws IOException { + int b = super.read(); + if (b >= 0 && bo != null) { + bo.write(b); + } + return b; + } + public int read(byte b[]) throws IOException { + return read(b, 0, b.length); + } + public int read(byte b[], int off, int len) throws IOException { + int res = super.read(b, off, len); + if (res > 0 && bo != null) { + bo.write(b, off, res); + } + return res; + } + public long skip(long n) throws IOException { + long res = super.skip(n); + if (res > 0 && bo != null) { + byte[] b = new byte[(int)res]; + bo.write(b, 0, (int)res); + } + return res; + } + public int available() throws IOException { + return super.available(); + } + public void close() throws IOException { + super.close(); + } + public void mark(int readlimit) { + super.mark(readlimit); + } + public void reset() throws IOException { + super.reset(); + } + public boolean markSupported() { + return false; + } + public void setSplit() { + bo = new ByteArrayOutputStream(); + } + public byte[] getSplit() { + if (bo == null) { + return null; + } + byte[] res = bo.toByteArray(); + bo = null; + return res; + } + public byte[] getSplitData() { + if(bo == null) + return null; + + return bo.toByteArray(); + } +} diff --git a/console/src/com/cloud/console/TileInfo.java b/console/src/com/cloud/console/TileInfo.java new file mode 100644 index 00000000000..ef917f32086 --- /dev/null +++ b/console/src/com/cloud/console/TileInfo.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.console; + +import java.awt.Rectangle; + +public class TileInfo { + private int row; + private int col; + private Rectangle tileRect; + + public TileInfo(int row, int col, Rectangle tileRect) { + this.row = row; + this.col = col; + this.tileRect = tileRect; + } + + public int getRow() { + return row; + } + + public void setRow(int row) { + this.row = row; + } + + public int getCol() { + return col; + } + + public void setCol(int col) { + this.col = col; + } + + public Rectangle getTileRect() { + return tileRect; + } + + public void setTileRect(Rectangle tileRect) { + this.tileRect = tileRect; + } +} diff --git a/console/src/com/cloud/console/TileTracker.java b/console/src/com/cloud/console/TileTracker.java new file mode 100644 index 00000000000..8cd02794a6c --- /dev/null +++ b/console/src/com/cloud/console/TileTracker.java @@ -0,0 +1,271 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.console; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.List; + +public class TileTracker { + + // 2 dimension tile status snapshot, a true value means the corresponding tile has been invalidated + private boolean[][] snapshot; + + private int tileWidth = 0; + private int tileHeight = 0; + private int trackWidth = 0; + private int trackHeight = 0; + + public TileTracker() { + } + + public int getTileWidth() { + return tileWidth; + } + + public void setTileWidth(int tileWidth) { + this.tileWidth = tileWidth; + } + + public int getTileHeight() { + return tileHeight; + } + + public void setTileHeight(int tileHeight) { + this.tileHeight = tileHeight; + } + + public int getTrackWidth() { + return trackWidth; + } + + public void setTrackWidth(int trackWidth) { + this.trackWidth = trackWidth; + } + + public int getTrackHeight() { + return trackHeight; + } + + public void setTrackHeight(int trackHeight) { + this.trackHeight = trackHeight; + } + + public void initTracking(int tileWidth, int tileHeight, int trackWidth, int trackHeight) { + assert(tileWidth > 0); + assert(tileHeight > 0); + assert(trackWidth > 0); + assert(trackHeight > 0); + assert(tileWidth <= trackWidth); + assert(tileHeight <= trackHeight); + + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + this.trackWidth = trackWidth; + this.trackHeight = trackHeight; + + int cols = getTileCols(); + int rows = getTileRows(); + snapshot = new boolean[rows][cols]; + for(int i = 0; i < rows; i++) + for(int j = 0; j < cols; j++) + snapshot[i][j] = false; + } + + public synchronized void resize(int trackWidth, int trackHeight) { + assert(tileWidth > 0); + assert(tileHeight > 0); + assert(trackWidth > 0); + assert(trackHeight > 0); + + this.trackWidth = trackWidth; + this.trackHeight = trackHeight; + + int cols = getTileCols(); + int rows = getTileRows(); + snapshot = new boolean[rows][cols]; + for(int i = 0; i < rows; i++) + for(int j = 0; j < cols; j++) + snapshot[i][j] = true; + } + + public void invalidate(Rectangle rect) { + setTileFlag(rect, true); + } + + public void validate(Rectangle rect) { + setTileFlag(rect, false); + } + + public List scan(boolean init) { + List l = new ArrayList(); + + synchronized(this) { + for(int i = 0; i < getTileRows(); i++) { + for(int j = 0; j < getTileCols(); j++) { + if(init || snapshot[i][j]) { + Rectangle rect = new Rectangle(); + rect.y = i*tileHeight; + rect.x = j*tileWidth; + rect.width = Math.min(trackWidth - rect.x, tileWidth); + rect.height = Math.min(trackHeight - rect.y, tileHeight); + + l.add(new TileInfo(i, j, rect)); + snapshot[i][j] = false; + } + } + } + + return l; + } + } + + public boolean hasFullCoverage() { + synchronized(this) { + for(int i = 0; i < getTileRows(); i++) { + for(int j = 0; j < getTileCols(); j++) { + if(!snapshot[i][j]) + return false; + } + } + } + return true; + } + + + + public void initCoverageTest() { + synchronized(this) { + for(int i = 0; i < getTileRows(); i++) { + for(int j = 0; j < getTileCols(); j++) { + snapshot[i][j] = false; + } + } + } + } + + // listener will be called while holding the object lock, use it + // with care to avoid deadlock condition being formed + public synchronized void scan(int nStartRow, int nStartCol, ITileScanListener listener) { + assert(listener != null); + + int cols = getTileCols(); + int rows = getTileRows(); + + nStartRow = nStartRow % rows; + nStartCol = nStartCol % cols; + + int nPos = nStartRow*cols + nStartCol; + int nUnits = rows*cols; + int nStartPos = nPos; + int nRow; + int nCol; + do { + nRow = nPos / cols; + nCol = nPos % cols; + + if(snapshot[nRow][nCol]) { + int nEndCol = nCol; + for(; nEndCol < cols && snapshot[nRow][nEndCol]; nEndCol++) { + snapshot[nRow][nEndCol] = false; + } + + Rectangle rect = new Rectangle(); + rect.y = nRow*tileHeight; + rect.height = tileHeight; + rect.x = nCol*tileWidth; + rect.width = (nEndCol - nCol)*tileWidth; + + if(!listener.onTileChange(rect, nRow, nEndCol)) + break; + } + + nPos = (nPos + 1) % nUnits; + } while(nPos != nStartPos); + } + + public void capture(ITileScanListener listener) { + assert(listener != null); + + int cols = getTileCols(); + int rows = getTileRows(); + + RegionClassifier classifier = new RegionClassifier(); + int left, top, right, bottom; + + synchronized(this) { + for(int i = 0; i < rows; i++) { + top = i*tileHeight; + bottom = Math.min(top + tileHeight, trackHeight); + for(int j = 0; j < cols; j++) { + left = j*tileWidth; + right = Math.min(left + tileWidth, trackWidth); + + if(snapshot[i][j]) { + snapshot[i][j] = false; + classifier.add(new Rectangle(left, top, right - left, bottom - top)); + } + } + } + } + listener.onRegionChange(classifier.getRegionList()); + } + + private synchronized void setTileFlag(Rectangle rect, boolean flag) { + int nStartTileRow; + int nStartTileCol; + int nEndTileRow; + int nEndTileCol; + + int cols = getTileCols(); + int rows = getTileRows(); + + if(rect != null) { + nStartTileRow = Math.min(getTileYPos(rect.y), rows - 1); + nStartTileCol = Math.min(getTileXPos(rect.x), cols - 1); + nEndTileRow = Math.min(getTileYPos(rect.y + rect.height - 1), rows -1); + nEndTileCol = Math.min(getTileXPos(rect.x + rect.width - 1), cols -1); + } else { + nStartTileRow = 0; + nStartTileCol = 0; + nEndTileRow = rows - 1; + nEndTileCol = cols - 1; + } + + for(int i = nStartTileRow; i <= nEndTileRow; i++) + for(int j = nStartTileCol; j <= nEndTileCol; j++) + snapshot[i][j] = flag; + } + + private int getTileRows() { + return (trackHeight + tileHeight - 1) / tileHeight; + } + + private int getTileCols() { + return (trackWidth + tileWidth - 1) / tileWidth; + } + + private int getTileXPos(int x) { + return x / tileWidth; + } + + public int getTileYPos(int y) { + return y / tileHeight; + } +} diff --git a/console/src/com/cloud/console/ZlibInStream.java b/console/src/com/cloud/console/ZlibInStream.java new file mode 100644 index 00000000000..1ef5ed15763 --- /dev/null +++ b/console/src/com/cloud/console/ZlibInStream.java @@ -0,0 +1,114 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +// repackage it to VMOps common packaging structure +package com.cloud.console; + +// +// A ZlibInStream reads from a zlib.io.InputStream +// + +public class ZlibInStream extends InStream { + + static final int defaultBufSize = 16384; + + public ZlibInStream(int bufSize_) { + bufSize = bufSize_; + b = new byte[bufSize]; + ptr = end = ptrOffset = 0; + inflater = new java.util.zip.Inflater(); + } + + public ZlibInStream() { this(defaultBufSize); } + + public void setUnderlying(InStream is, int bytesIn_) { + underlying = is; + bytesIn = bytesIn_; + ptr = end = 0; + } + + public void reset() throws Exception { + ptr = end = 0; + if (underlying == null) return; + + while (bytesIn > 0) { + decompress(); + end = 0; // throw away any data + } + underlying = null; + } + + public int pos() { return ptrOffset + ptr; } + + protected int overrun(int itemSize, int nItems) throws Exception { + if (itemSize > bufSize) + throw new Exception("ZlibInStream overrun: max itemSize exceeded"); + if (underlying == null) + throw new Exception("ZlibInStream overrun: no underlying stream"); + + if (end - ptr != 0) + System.arraycopy(b, ptr, b, 0, end - ptr); + + ptrOffset += ptr; + end -= ptr; + ptr = 0; + + while (end < itemSize) { + decompress(); + } + + if (itemSize * nItems > end) + nItems = end / itemSize; + + return nItems; + } + + // decompress() calls the decompressor once. Note that this won't + // necessarily generate any output data - it may just consume some input + // data. Returns false if wait is false and we would block on the underlying + // stream. + + private void decompress() throws Exception { + try { + underlying.check(1); + int avail_in = underlying.getend() - underlying.getptr(); + if (avail_in > bytesIn) + avail_in = bytesIn; + + if (inflater.needsInput()) { + inflater.setInput(underlying.getbuf(), underlying.getptr(), avail_in); + } + + int n = inflater.inflate(b, end, bufSize - end); + + end += n; + if (inflater.needsInput()) { + bytesIn -= avail_in; + underlying.setptr(underlying.getptr() + avail_in); + } + } catch (java.util.zip.DataFormatException e) { + throw new Exception("ZlibInStream: inflate failed"); + } + } + + private InStream underlying; + private int bufSize; + private int ptrOffset; + private java.util.zip.Inflater inflater; + private int bytesIn; +} diff --git a/core/.classpath b/core/.classpath new file mode 100644 index 00000000000..d5ea1df23fc --- /dev/null +++ b/core/.classpath @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/.project b/core/.project new file mode 100644 index 00000000000..9884df8b9ca --- /dev/null +++ b/core/.project @@ -0,0 +1,17 @@ + + + core + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/core/src/com/cloud/agent/AgentManager.java b/core/src/com/cloud/agent/AgentManager.java new file mode 100755 index 00000000000..08e9a30a4ee --- /dev/null +++ b/core/src/com/cloud/agent/AgentManager.java @@ -0,0 +1,213 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent; + +import java.net.URI; +import java.util.List; +import java.util.Set; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.HostPodVO; +import com.cloud.dc.PodCluster; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.DiscoveryException; +import com.cloud.exception.InternalErrorException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.host.HostStats; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.Status.Event; +import com.cloud.offering.ServiceOffering; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.storage.StoragePoolVO; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VirtualMachineTemplate; +import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; +import com.cloud.utils.component.Manager; +import com.cloud.vm.VMInstanceVO; + +/** + * AgentManager manages hosts. It directly coordinates between the + * DAOs and the connections it manages. + */ +public interface AgentManager extends Manager { + + /** + * easy send method that returns null if there's any errors. It handles all exceptions. + * + * @param hostId host id + * @param cmd command to send. + * @return Answer if successful; null if not. + */ + Answer easySend(Long hostId, Command cmd); + + /** + * Synchronous sending a command to the agent. + * + * @param hostId id of the agent on host + * @param cmd command + * @return an Answer + */ + Answer send(Long hostId, Command cmd, int timeout) throws AgentUnavailableException, OperationTimedoutException; + + Answer send(Long hostId, Command cmd) throws AgentUnavailableException, OperationTimedoutException; + + /** + * Synchronous sending a list of commands to the agent. + * + * @param hostId id of the agent on host + * @param cmds array of commands + * @param isControl Commands sent contains control commands + * @param stopOnError should the agent stop execution on the first error. + * @return an array of Answer + */ + Answer[] send(Long hostId, Command [] cmds, boolean stopOnError) throws AgentUnavailableException, OperationTimedoutException; + + Answer[] send(Long hostId, Command [] cmds, boolean stopOnError, int timeout) throws AgentUnavailableException, OperationTimedoutException; + + /** + * Asynchronous sending of a command to the agent. + * @param hostId id of the agent on the host. + * @param cmd Command to send. + * @param listener the listener to process the answer. + * @return sequence number. + */ + long gatherStats(Long hostId, Command cmd, Listener listener); + + /** + * Asynchronous sending of a command to the agent. + * @param hostId id of the agent on the host. + * @param cmds Commands to send. + * @param stopOnError should the agent stop execution on the first error. + * @param listener the listener to process the answer. + * @return sequence number. + */ + long send(Long hostId, Command[] cmds, boolean stopOnError, Listener listener) throws AgentUnavailableException; + + /** + * Register to listen for host events. These are mostly connection and + * disconnection events. + * + * @param listener + * @param connections listen for connections + * @param commands listen for connections + * @param priority in listening for events. + * @return id to unregister if needed. + */ + int registerForHostEvents(Listener listener, boolean connections, boolean commands, boolean priority); + + /** + * Unregister for listening to host events. + * @param id returned from registerForHostEvents + */ + void unregisterForHostEvents(int id); + + /** + * @return hosts currently connected. + */ + Set getConnectedHosts(); + + /** + * Disconnect the agent. + * + * @param hostId host to disconnect. + * @param reason the reason why we're disconnecting. + * + */ + void disconnect(long hostId, Status.Event event, boolean investigate); + + /** + * Obtains statistics for a host; vCPU utilisation, memory utilisation, and network utilisation + * @param hostId + * @return HostStats + * @throws InternalErrorException + */ + HostStats getHostStatistics(long hostId) throws InternalErrorException; + + Long getGuestOSCategoryId(long hostId); + + /** + * Find a host based on the type needed, data center to deploy in, pod + * to deploy in, service offering, template, and list of host to avoid. + */ + + Host findHost(Host.Type type, DataCenterVO dc, HostPodVO pod, StoragePoolVO sp, ServiceOffering offering, VMTemplateVO template, VMInstanceVO vm, Host currentHost, Set avoid); + List listByDataCenter(long dcId); + List listByPod(long podId); + + /** + * Updates a host + * @param hostId + * @param guestOSCategoryId + */ + void updateHost(long hostId, long guestOSCategoryId); + + /** + * Deletes a host + * + * @param hostId + * @param true if deleted, false otherwise + */ + boolean deleteHost(long hostId); + + /** + * Find a pod based on the user id, template, and data center. + * + * @param template + * @param dc + * @param userId + * @return + */ + Pair findPod(VirtualMachineTemplate template, ServiceOfferingVO offering, DataCenterVO dc, long userId, Set avoids); + + /** + * Put the agent in maintenance mode. + * + * @param hostId id of the host to put in maintenance mode. + * @return true if it was able to put the agent into maintenance mode. false if not. + */ + boolean maintain(long hostId) throws AgentUnavailableException; + + boolean maintenanceFailed(long hostId); + + /** + * Cancel the maintenance mode. + * + * @param hostId host id + * @return true if it's done. false if not. + */ + boolean cancelMaintenance(long hostId); + + /** + * Check to see if a virtual machine can be upgraded to the given service offering + * + * @param vm + * @param offering + * @return true if the host can handle the upgrade, false otherwise + */ + boolean isVirtualMachineUpgradable(final UserVm vm, final ServiceOffering offering); + + public boolean executeUserRequest(long hostId, Event event) throws AgentUnavailableException; + public boolean reconnect(final long hostId) throws AgentUnavailableException; + + public List discoverHosts(long dcId, Long podId, Long clusterId, URI url, String username, String password) throws DiscoveryException; +} diff --git a/core/src/com/cloud/agent/IAgentControl.java b/core/src/com/cloud/agent/IAgentControl.java new file mode 100644 index 00000000000..eb6765c6c93 --- /dev/null +++ b/core/src/com/cloud/agent/IAgentControl.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent; + + +import com.cloud.agent.api.AgentControlAnswer; +import com.cloud.agent.api.AgentControlCommand; +import com.cloud.exception.AgentControlChannelException; + +public interface IAgentControl { + void registerControlListener(IAgentControlListener listener); + void unregisterControlListener(IAgentControlListener listener); + + AgentControlAnswer sendRequest(AgentControlCommand cmd, int timeoutInMilliseconds) throws AgentControlChannelException; + void postRequest(AgentControlCommand cmd) throws AgentControlChannelException; +} diff --git a/core/src/com/cloud/agent/IAgentControlListener.java b/core/src/com/cloud/agent/IAgentControlListener.java new file mode 100644 index 00000000000..6ee7e482d9a --- /dev/null +++ b/core/src/com/cloud/agent/IAgentControlListener.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent; + +import com.cloud.agent.api.AgentControlAnswer; +import com.cloud.agent.api.AgentControlCommand; +import com.cloud.agent.api.Answer; +import com.cloud.agent.transport.Request; +import com.cloud.agent.transport.Response; + +public interface IAgentControlListener { + public Answer processControlRequest(Request request, AgentControlCommand cmd); + public void processControlResponse(Response response, AgentControlAnswer answer); +} diff --git a/core/src/com/cloud/agent/Listener.java b/core/src/com/cloud/agent/Listener.java new file mode 100755 index 00000000000..260465edcf1 --- /dev/null +++ b/core/src/com/cloud/agent/Listener.java @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent; + +import com.cloud.agent.api.AgentControlAnswer; +import com.cloud.agent.api.AgentControlCommand; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.StartupCommand; +import com.cloud.host.HostVO; +import com.cloud.host.Status; + +/** + * Listener is a multipurpose interface for hooking into the AgentManager. + * There are several types of events that the AgentManager forwards + * to the listener. + * + * 1. Agent Connect & Disconnect + * 2. Commands sent by the agent. + * 3. Answers sent by the agent. + */ +public interface Listener { + + /** + * If the Listener is passed in the send(), this method will + * be called to process the answers. + * + * @param agentId id of the agent + * @param seq sequence number return by the send() method. + * @param answers answers to the commands. + * @return true if processed. false if not. + */ + boolean processAnswer(long agentId, long seq, Answer[] answers); + + /** + * This method is called by the AgentManager when an agent sent + * a command to the server. In order to process these commands, + * the Listener must be registered for host commands. + * + * @param agentId id of the agent. + * @param seq sequence number of the command sent. + * @param commands commands that were sent. + * @return true if you processed the commands. false if not. + */ + boolean processCommand(long agentId, long seq, Command[] commands); + + /** + * process control command sent from agent under its management + * @param agentId + * @param cmd + * @return + */ + AgentControlAnswer processControlCommand(long agentId, AgentControlCommand cmd); + + /** + * This method is called by AgentManager when an agent made a + * connection to this server if the listener has + * been registered for host events. + * @param agentId id of the agent + * @param cmd command sent by the agent to the server on startup. + */ + boolean processConnect(HostVO host, StartupCommand cmd); + + /** + * This method is called by AgentManager when an agent disconnects + * from this server if the listener has been registered for host events. + * + * If the Listener is passed to the send() method, this method is + * also called by AgentManager if the agent disconnected. + * + * @param agentId id of the agent + * @param state the current state of the agent. + */ + boolean processDisconnect(long agentId, Status state); + + /** + * If ths Listener is passed to the send() method, this method + * is called by AgentManager after processing an answer + * from the agent. Returning true means you're expecting more + * answers from the agent using the same sequence number. + * + * @return true if expecting more answers using the same sequence number. + */ + boolean isRecurring(); + + /** + * If the Listener is passed to the send() method, this method is + * called to determine how long to wait for the reply. The value + * is in seconds. -1 indicates to wait forever. 0 indicates to + * use the default timeout. If the timeout is + * reached, processTimeout on this same Listener is called. + * + * @return timeout in seconds before processTimeout is called. + */ + int getTimeout(); + + /** + * If the Listener is passed to the send() method, this method is + * called by the AgentManager to process a command timeout. + * @param agentId id of the agent + * @param seq sequence number returned by the send(). + * @return true if processed; false if not. + */ + boolean processTimeout(long agentId, long seq); +} diff --git a/core/src/com/cloud/agent/RecoveryHandler.java b/core/src/com/cloud/agent/RecoveryHandler.java new file mode 100644 index 00000000000..6dfe6308253 --- /dev/null +++ b/core/src/com/cloud/agent/RecoveryHandler.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent; + +import com.cloud.agent.api.Command; + +public interface RecoveryHandler { + /** + * Perform the necessary recovery because the success of this command + * is not known. + * + * @param agentId agent the commands were sent to. + * @param seq sequence number. + * @param cmds commands that failed. + */ + public void handle(long agentId, long seq, Command[] cmds); +} diff --git a/core/src/com/cloud/agent/api/AbstractStartCommand.java b/core/src/com/cloud/agent/api/AbstractStartCommand.java new file mode 100644 index 00000000000..b2a943ad5f0 --- /dev/null +++ b/core/src/com/cloud/agent/api/AbstractStartCommand.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +import java.util.List; + +import com.cloud.storage.VolumeVO; +import com.cloud.storage.VirtualMachineTemplate.BootloaderType; + +public abstract class AbstractStartCommand extends Command { + + protected String vmName; + protected String storageHosts[] = new String[2]; + protected List volumes; + protected boolean mirroredVols = false; + protected BootloaderType bootloader = BootloaderType.PyGrub; + + public AbstractStartCommand(String vmName, String storageHost, List vols) { + this(vmName, new String[] {storageHost}, vols, false); + } + + public AbstractStartCommand(String vmName, String[] storageHosts, List volumes, boolean mirroredVols) { + super(); + this.vmName = vmName; + this.storageHosts = storageHosts; + this.volumes = volumes; + this.mirroredVols = mirroredVols; + } + + public BootloaderType getBootloader() { + return bootloader; + } + + public void setBootloader(BootloaderType bootloader) { + this.bootloader = bootloader; + } + + protected AbstractStartCommand() { + super(); + } + + public List getVolumes() { + return volumes; + } + + public String getVmName() { + return vmName; + } + + public String getStorageHost() { + return storageHosts[0]; + } + + public boolean isMirroredVols() { + return mirroredVols; + } + + public void setMirroredVols(boolean mirroredVols) { + this.mirroredVols = mirroredVols; + } + + public String [] getStorageHosts() { + return storageHosts; + } + +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/AgentControlAnswer.java b/core/src/com/cloud/agent/api/AgentControlAnswer.java new file mode 100644 index 00000000000..4bc48199fae --- /dev/null +++ b/core/src/com/cloud/agent/api/AgentControlAnswer.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class AgentControlAnswer extends Answer { + public AgentControlAnswer() { + } + + public AgentControlAnswer(Command command) { + super(command); + } + + public AgentControlAnswer(Command command, boolean success, String details) { + super(command, success, details); + } +} diff --git a/core/src/com/cloud/agent/api/AgentControlCommand.java b/core/src/com/cloud/agent/api/AgentControlCommand.java new file mode 100644 index 00000000000..aaf9bcefddd --- /dev/null +++ b/core/src/com/cloud/agent/api/AgentControlCommand.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class AgentControlCommand extends Command { + + public AgentControlCommand() { + } + + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/Answer.java b/core/src/com/cloud/agent/api/Answer.java new file mode 100755 index 00000000000..449dadfe52a --- /dev/null +++ b/core/src/com/cloud/agent/api/Answer.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.utils.exception.ExceptionUtil; + +public class Answer extends Command { + boolean result; + String details; + + protected Answer() { + } + + public Answer(Command command) { + this(command, true, null); + } + + public Answer(Command command, boolean success, String details) { + result = success; + this.details = details; + } + + public Answer(Command command, Exception e) { + this(command, false, ExceptionUtil.toString(e)); + } + + public boolean getResult() { + return result; + } + + public String getDetails() { + return details; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public static UnsupportedAnswer createUnsupportedCommandAnswer(Command cmd) { + return new UnsupportedAnswer(cmd, "Unsupported command issued:" + cmd.toString() + ". Are you sure you got the right type of server?"); + } + + public static UnsupportedAnswer createUnsupportedVersionAnswer(Command cmd) { + return new UnsupportedAnswer(cmd, "Unsuppored Version."); + } +} diff --git a/core/src/com/cloud/agent/api/AttachIsoCommand.java b/core/src/com/cloud/agent/api/AttachIsoCommand.java new file mode 100644 index 00000000000..6073ab553f7 --- /dev/null +++ b/core/src/com/cloud/agent/api/AttachIsoCommand.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class AttachIsoCommand extends Command { + + private String vmName; + private String isoPath; + private boolean attach; + + protected AttachIsoCommand() { + } + + public AttachIsoCommand(String vmName, String isoPath, boolean attach) { + this.vmName = vmName; + this.isoPath = isoPath; + this.attach = attach; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String getVmName() { + return vmName; + } + + public String getIsoPath() { + return isoPath; + } + + public boolean isAttach() { + return attach; + } +} diff --git a/core/src/com/cloud/agent/api/AttachVolumeAnswer.java b/core/src/com/cloud/agent/api/AttachVolumeAnswer.java new file mode 100644 index 00000000000..22b84b268b2 --- /dev/null +++ b/core/src/com/cloud/agent/api/AttachVolumeAnswer.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +public class AttachVolumeAnswer extends Answer { + private Long deviceId; + + protected AttachVolumeAnswer() { + + } + + public AttachVolumeAnswer(AttachVolumeCommand cmd, String result) { + super(cmd, false, result); + this.deviceId = null; + } + + public AttachVolumeAnswer(AttachVolumeCommand cmd, Long deviceId) { + super(cmd); + this.deviceId = deviceId; + } + + + public AttachVolumeAnswer(AttachVolumeCommand cmd) { + super(cmd); + this.deviceId = null; + } + /** + * @return the deviceId + */ + public Long getDeviceId() { + return deviceId; + } +} diff --git a/core/src/com/cloud/agent/api/AttachVolumeCommand.java b/core/src/com/cloud/agent/api/AttachVolumeCommand.java new file mode 100644 index 00000000000..ca07733a91e --- /dev/null +++ b/core/src/com/cloud/agent/api/AttachVolumeCommand.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +import com.cloud.storage.Storage.StoragePoolType; + +public class AttachVolumeCommand extends Command { + + boolean attach; + String vmName; + StoragePoolType pooltype; + String volumeFolder; + String volumePath; + String volumeName; + Long deviceId; + + protected AttachVolumeCommand() { + } + + public AttachVolumeCommand(boolean attach, String vmName, StoragePoolType pooltype, String volumeFolder, String volumePath, String volumeName, Long deviceId) { + this.attach = attach; + this.vmName = vmName; + this.pooltype = pooltype; + this.volumeFolder = volumeFolder; + this.volumePath = volumePath; + this.volumeName = volumeName; + this.deviceId = deviceId; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public boolean getAttach() { + return attach; + } + + public String getVmName() { + return vmName; + } + + public StoragePoolType getPooltype() { + return pooltype; + } + + public void setPooltype(StoragePoolType pooltype) { + this.pooltype = pooltype; + } + + public String getVolumeFolder() { + return volumeFolder; + } + + public String getVolumePath() { + return volumePath; + } + + public String getVolumeName() { + return volumeName; + } + + public Long getDeviceId() { + return deviceId; + } + + public void setDeviceId(Long deviceId) { + this.deviceId = deviceId; + } + +} diff --git a/core/src/com/cloud/agent/api/BackupSnapshotAnswer.java b/core/src/com/cloud/agent/api/BackupSnapshotAnswer.java new file mode 100644 index 00000000000..93b5fe78520 --- /dev/null +++ b/core/src/com/cloud/agent/api/BackupSnapshotAnswer.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +public class BackupSnapshotAnswer extends Answer { + private String backupSnapshotName; + + protected BackupSnapshotAnswer() { + + } + + public BackupSnapshotAnswer(BackupSnapshotCommand cmd, boolean success, String result, String backupSnapshotName) { + super(cmd, success, result); + this.backupSnapshotName = backupSnapshotName; + } + + /** + * @return the backupSnapshotName + */ + public String getBackupSnapshotName() { + return backupSnapshotName; + } +} diff --git a/core/src/com/cloud/agent/api/BackupSnapshotCommand.java b/core/src/com/cloud/agent/api/BackupSnapshotCommand.java new file mode 100644 index 00000000000..ccf45d71115 --- /dev/null +++ b/core/src/com/cloud/agent/api/BackupSnapshotCommand.java @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +/** + * When a snapshot of a VDI is taken, it creates two new files, + * a 'base copy' which contains all the new data since the time of the last snapshot and an 'empty snapshot' file. + * Any new data is again written to the VDI with the same UUID. + * This class issues a command for copying the 'base copy' vhd file to secondary storage. + * This currently assumes that both primary and secondary storage are mounted on the XenServer. + */ +public class BackupSnapshotCommand extends SnapshotCommand { + private String prevSnapshotUuid; + private String prevBackupUuid; + private boolean isFirstSnapshotOfRootVolume; + private boolean isVolumeInactive; + private String firstBackupUuid; + private String vmName; + + protected BackupSnapshotCommand() { + + } + + /** + * @param primaryStoragePoolNameLabel The UUID of the primary storage Pool + * @param secondaryStoragePoolURL This is what shows up in the UI when you click on Secondary storage. + * @param snapshotUuid The UUID of the snapshot which is going to be backed up + * @param prevSnapshotUuid The UUID of the previous snapshot for this volume. This will be destroyed on the primary storage. + * @param prevBackupUuid This is the UUID of the vhd file which was last backed up on secondary storage. + * @param firstBackupUuid This is the backup of the first ever snapshot taken by the volume. + * @param isFirstSnapshotOfRootVolume true if this is the first snapshot of a root volume. Set the parent of the backup to null. + * @param isVolumeInactive True if the volume belongs to a VM that is not running or is detached. + */ + public BackupSnapshotCommand(String primaryStoragePoolNameLabel, + String secondaryStoragePoolURL, + Long dcId, + Long accountId, + Long volumeId, + String snapshotUuid, + String snapshotName, + String prevSnapshotUuid, + String prevBackupUuid, + String firstBackupUuid, + boolean isFirstSnapshotOfRootVolume, + boolean isVolumeInactive, + String vmName) + { + super(primaryStoragePoolNameLabel, secondaryStoragePoolURL, snapshotUuid, snapshotName, dcId, accountId, volumeId); + this.prevSnapshotUuid = prevSnapshotUuid; + this.prevBackupUuid = prevBackupUuid; + this.firstBackupUuid = firstBackupUuid; + this.isFirstSnapshotOfRootVolume = isFirstSnapshotOfRootVolume; + this.isVolumeInactive = isVolumeInactive; + this.vmName = vmName; + } + + public String getPrevSnapshotUuid() { + return prevSnapshotUuid; + } + + public String getPrevBackupUuid() { + return prevBackupUuid; + } + + public String getFirstBackupUuid() { + return firstBackupUuid; + } + + public boolean isFirstSnapshotOfRootVolume() { + return isFirstSnapshotOfRootVolume; + } + + public boolean isVolumeInactive() { + return isVolumeInactive; + } + + public String getVmName() { + return vmName; + } +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/CancelCommand.java b/core/src/com/cloud/agent/api/CancelCommand.java new file mode 100644 index 00000000000..3565e6c2dbf --- /dev/null +++ b/core/src/com/cloud/agent/api/CancelCommand.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + + +public class CancelCommand extends Command { + protected long sequence; + protected String reason; + + protected CancelCommand() { + } + + public CancelCommand(long sequence, String reason) { + this.sequence = sequence; + this.reason = reason; + } + + public long getSequence() { + return sequence; + } + + public String getReason() { + return reason; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/ChangeAgentAnswer.java b/core/src/com/cloud/agent/api/ChangeAgentAnswer.java new file mode 100644 index 00000000000..d15c06703b3 --- /dev/null +++ b/core/src/com/cloud/agent/api/ChangeAgentAnswer.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class ChangeAgentAnswer extends Answer { + protected ChangeAgentAnswer() { + } + + public ChangeAgentAnswer(ChangeAgentCommand cmd, boolean result) { + super(cmd, result, null); + } +} diff --git a/core/src/com/cloud/agent/api/ChangeAgentCommand.java b/core/src/com/cloud/agent/api/ChangeAgentCommand.java new file mode 100644 index 00000000000..5d1d411387a --- /dev/null +++ b/core/src/com/cloud/agent/api/ChangeAgentCommand.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.host.Status.Event; + +public class ChangeAgentCommand extends Command { + long agentId; + Event event; + + protected ChangeAgentCommand() { + } + + public ChangeAgentCommand(long agentId, Event event) { + this.agentId = agentId; + this.event = event; + } + + public long getAgentId() { + return agentId; + } + + public Event getEvent() { + return event; + } + + @Override + public boolean executeInSequence() { + return false; + } + +} diff --git a/core/src/com/cloud/agent/api/CheckHealthAnswer.java b/core/src/com/cloud/agent/api/CheckHealthAnswer.java new file mode 100644 index 00000000000..4441ae96d08 --- /dev/null +++ b/core/src/com/cloud/agent/api/CheckHealthAnswer.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class CheckHealthAnswer extends Answer { + + public CheckHealthAnswer() {} + + public CheckHealthAnswer(CheckHealthCommand cmd, boolean alive) { + super(cmd, alive, "resource is " + (alive? "alive" : "not alive")); + } +} diff --git a/core/src/com/cloud/agent/api/CheckHealthCommand.java b/core/src/com/cloud/agent/api/CheckHealthCommand.java new file mode 100644 index 00000000000..f78d7cb49aa --- /dev/null +++ b/core/src/com/cloud/agent/api/CheckHealthCommand.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class CheckHealthCommand extends Command { + + public CheckHealthCommand() {} + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/CheckOnHostAnswer.java b/core/src/com/cloud/agent/api/CheckOnHostAnswer.java new file mode 100644 index 00000000000..72d98873943 --- /dev/null +++ b/core/src/com/cloud/agent/api/CheckOnHostAnswer.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class CheckOnHostAnswer extends Answer { + boolean determined; + boolean alive; + + protected CheckOnHostAnswer() { + } + + public CheckOnHostAnswer(CheckOnHostCommand cmd, Boolean alive, String details) { + super(cmd, true, details); + if (alive == null) { + determined = false; + } else { + determined = true; + this.alive = alive; + } + } + + public CheckOnHostAnswer(CheckOnHostCommand cmd, String details) { + super(cmd, false, details); + } + + public boolean isDetermined() { + return determined; + } + + public boolean isAlive() { + return alive; + } + +} diff --git a/core/src/com/cloud/agent/api/CheckOnHostCommand.java b/core/src/com/cloud/agent/api/CheckOnHostCommand.java new file mode 100644 index 00000000000..38f808713ba --- /dev/null +++ b/core/src/com/cloud/agent/api/CheckOnHostCommand.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.agent.api.to.HostTO; +import com.cloud.host.HostVO; + +public class CheckOnHostCommand extends Command { + HostTO host; + + protected CheckOnHostCommand() { + } + + + public CheckOnHostCommand(HostVO host) { + this.host = new HostTO(host); + } + + public HostTO getHost() { + return host; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/CheckOnVmAnswer.java b/core/src/com/cloud/agent/api/CheckOnVmAnswer.java new file mode 100644 index 00000000000..84885962223 --- /dev/null +++ b/core/src/com/cloud/agent/api/CheckOnVmAnswer.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class CheckOnVmAnswer extends Answer { + boolean determined; + boolean alive; + + protected CheckOnVmAnswer() { + } + + public CheckOnVmAnswer(CheckOnVmCommand cmd, Boolean alive) { + this(cmd, alive, null); + } + + public CheckOnVmAnswer(CheckOnVmCommand cmd, Boolean alive, String details) { + super(cmd, true, details); + if (alive == null) { + determined = false; + } else { + determined = true; + this.alive = alive; + } + } + + public CheckOnVmAnswer(CheckOnVmCommand cmd, Exception e) { + super(cmd, e); + } + + public boolean isDetermined() { + return determined; + } + + public boolean isAlive() { + return alive; + } +} diff --git a/core/src/com/cloud/agent/api/CheckOnVmCommand.java b/core/src/com/cloud/agent/api/CheckOnVmCommand.java new file mode 100644 index 00000000000..09275570aa0 --- /dev/null +++ b/core/src/com/cloud/agent/api/CheckOnVmCommand.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.host.HostVO; +import com.cloud.vm.VMInstanceVO; + +/** + * @author ahuang + * + */ +public class CheckOnVmCommand extends Command { + + protected CheckOnVmCommand() { + } + + public CheckOnVmCommand(VMInstanceVO vm, HostVO host) { + + } + + @Override + public boolean executeInSequence() { + return false; + } + +} diff --git a/core/src/com/cloud/agent/api/CheckStateAnswer.java b/core/src/com/cloud/agent/api/CheckStateAnswer.java new file mode 100755 index 00000000000..359fbf39437 --- /dev/null +++ b/core/src/com/cloud/agent/api/CheckStateAnswer.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.vm.State; + +/** + * This returns an answer on the state of the VM. If the state, is Error + * or Unknown, the details should give more details on what is wrong. + */ +public class CheckStateAnswer extends Answer { + State state; + + public CheckStateAnswer() {} + + public CheckStateAnswer(CheckStateCommand cmd, State state) { + this(cmd, state, null); + } + + public CheckStateAnswer(CheckStateCommand cmd, String details) { + super(cmd, false, details); + this.state = null; + } + + public CheckStateAnswer(CheckStateCommand cmd, State state, String details) { + super(cmd, true, details); + this.state = state; + } + + public State getState() { + return state; + } +} diff --git a/core/src/com/cloud/agent/api/CheckStateCommand.java b/core/src/com/cloud/agent/api/CheckStateCommand.java new file mode 100755 index 00000000000..37089a0e9fb --- /dev/null +++ b/core/src/com/cloud/agent/api/CheckStateCommand.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +/** + * + * CheckStateCommand is sent to the agent to check the state of a VM. + * + */ +public class CheckStateCommand extends Command { + String vmName; + + public CheckStateCommand() {} + + public CheckStateCommand(String vmName) { + this.vmName = vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String getVmName() { + return vmName; + } +} diff --git a/core/src/com/cloud/agent/api/CheckVirtualMachineAnswer.java b/core/src/com/cloud/agent/api/CheckVirtualMachineAnswer.java new file mode 100644 index 00000000000..5ff1e41dbd8 --- /dev/null +++ b/core/src/com/cloud/agent/api/CheckVirtualMachineAnswer.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.vm.State; + +public class CheckVirtualMachineAnswer extends Answer { + + Integer vncPort; + State state; + + + protected CheckVirtualMachineAnswer() { + } + + public CheckVirtualMachineAnswer(CheckVirtualMachineCommand cmd, State state, Integer vncPort, String detail) { + super(cmd, true, detail); + this.state = state; + this.vncPort = vncPort; + } + + public CheckVirtualMachineAnswer(CheckVirtualMachineCommand cmd, State state, Integer vncPort) { + this(cmd, state, vncPort, null); + } + + public CheckVirtualMachineAnswer(CheckVirtualMachineCommand cmd, String detail) { + super(cmd, false, detail); + } + + + public Integer getVncPort() { + return vncPort; + } + + public State getState() { + return state; + } +} diff --git a/core/src/com/cloud/agent/api/CheckVirtualMachineCommand.java b/core/src/com/cloud/agent/api/CheckVirtualMachineCommand.java new file mode 100644 index 00000000000..3eea2979b4b --- /dev/null +++ b/core/src/com/cloud/agent/api/CheckVirtualMachineCommand.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class CheckVirtualMachineCommand extends Command { + + private String vmName; + + protected CheckVirtualMachineCommand() { + + } + + public CheckVirtualMachineCommand(String vmName) { + this.vmName = vmName; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return false; + } + +} diff --git a/core/src/com/cloud/agent/api/Command.java b/core/src/com/cloud/agent/api/Command.java new file mode 100755 index 00000000000..68f492f972f --- /dev/null +++ b/core/src/com/cloud/agent/api/Command.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +/** + * Command is a command that is sent between the management agent and management + * server. Parameter and Command are loosely connected. The protocol layer does + * not care what parameter is carried with which command. That tie in is made at + * a higher level than here. + * + * Parameter names can only be 4 characters long and is checked with an assert. + * The value of the parameter is basically an arbitrary length byte array. + */ +public abstract class Command { + + protected Command() { + } + + public String toString() { + return this.getClass().getSimpleName(); + } + + public abstract boolean executeInSequence(); +} diff --git a/core/src/com/cloud/agent/api/ConsoleAccessAuthenticationAnswer.java b/core/src/com/cloud/agent/api/ConsoleAccessAuthenticationAnswer.java new file mode 100644 index 00000000000..50fbb04b316 --- /dev/null +++ b/core/src/com/cloud/agent/api/ConsoleAccessAuthenticationAnswer.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class ConsoleAccessAuthenticationAnswer extends AgentControlAnswer { + + private boolean _success; + + public ConsoleAccessAuthenticationAnswer() { + } + + public ConsoleAccessAuthenticationAnswer(Command cmd, boolean success) { + super(cmd); + _success = success; + } + + public boolean succeeded() { + return _success; + } +} diff --git a/core/src/com/cloud/agent/api/ConsoleAccessAuthenticationCommand.java b/core/src/com/cloud/agent/api/ConsoleAccessAuthenticationCommand.java new file mode 100644 index 00000000000..f1c25c5bd2f --- /dev/null +++ b/core/src/com/cloud/agent/api/ConsoleAccessAuthenticationCommand.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class ConsoleAccessAuthenticationCommand extends AgentControlCommand { + + private String _vmId; + private String _sid; + + public ConsoleAccessAuthenticationCommand() { + } + + public ConsoleAccessAuthenticationCommand(String vmId, String sid) { + _vmId = vmId; + _sid = sid; + } + + public String getVmId() { + return _vmId; + } + + public String getSid() { + return _sid; + } +} diff --git a/core/src/com/cloud/agent/api/ConsoleProxyLoadReportCommand.java b/core/src/com/cloud/agent/api/ConsoleProxyLoadReportCommand.java new file mode 100644 index 00000000000..b84fc3bfc0e --- /dev/null +++ b/core/src/com/cloud/agent/api/ConsoleProxyLoadReportCommand.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class ConsoleProxyLoadReportCommand extends AgentControlCommand { + + private long _proxyVmId; + private String _loadInfo; + + public ConsoleProxyLoadReportCommand() { + } + + public ConsoleProxyLoadReportCommand(long proxyVmId, String loadInfo) { + _proxyVmId = proxyVmId; + _loadInfo = loadInfo; + } + + public long getProxyVmId() { + return _proxyVmId; + } + + public String getLoadInfo() { + return _loadInfo; + } +} diff --git a/core/src/com/cloud/agent/api/CreatePrivateTemplateFromSnapshotCommand.java b/core/src/com/cloud/agent/api/CreatePrivateTemplateFromSnapshotCommand.java new file mode 100644 index 00000000000..13581099af9 --- /dev/null +++ b/core/src/com/cloud/agent/api/CreatePrivateTemplateFromSnapshotCommand.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +/** + * This class encapsulates the information required for creating a new Volume from the backup of a snapshot. + * This currently assumes that both primary and secondary storage are mounted on the XenServer. + */ +public class CreatePrivateTemplateFromSnapshotCommand extends SnapshotCommand { + private String origTemplateInstallPath; + private Long newTemplateId; + private String templateName; + + protected CreatePrivateTemplateFromSnapshotCommand() { + + } + + /** + * Given the UUID of a backed up snapshot VHD file on the secondary storage, the execute of this command does + * 1) Get the parent chain of this VHD all the way up to the root, say VHDList + * 2) Copy all the files in the VHDlist to some temp location + * 3) Coalesce all the VHDs to one VHD which contains all the data of the volume. This invokes the DeletePreviousBackupCommand for each VHD + * 4) Rename the UUID of this VHD + * @param secondaryStoragePoolURL This is what shows up in the UI when you click on Secondary storage. + * In the code, it is present as: In the vmops.host_details table, there is a field mount.parent. This is the value of that field + * If you have better ideas on how to get it, you are welcome. + * @param backedUpSnapshotUuid This is the UUID of the vhd file corresponding to the snapshot id from which the data has to be restored. + * It may not be the UUID of the base copy of the snapshot, if no data was written since last snapshot. + * @param origTemplateInstallPath The install path of the original template VHD on the secondary + */ + public CreatePrivateTemplateFromSnapshotCommand(String primaryStoragePoolNameLabel, + String secondaryStoragePoolURL, + Long dcId, + Long accountId, + Long volumeId, + String backedUpSnapshotUuid, + String backedUpSnapshotName, + String origTemplateInstallPath, + Long newTemplateId, + String templateName) + { + super(primaryStoragePoolNameLabel, secondaryStoragePoolURL, backedUpSnapshotUuid, backedUpSnapshotName, dcId, accountId, volumeId); + this.origTemplateInstallPath = origTemplateInstallPath; + this.newTemplateId = newTemplateId; + this.templateName = templateName; + } + + /** + * @return the origTemplateInstallPath + */ + public String getOrigTemplateInstallPath() { + return origTemplateInstallPath; + } + + public Long getNewTemplateId() { + return newTemplateId; + } + + /** + * @return templateName + */ + public String getTemplateName() { + return templateName; + } +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/CreateVolumeFromSnapshotAnswer.java b/core/src/com/cloud/agent/api/CreateVolumeFromSnapshotAnswer.java new file mode 100644 index 00000000000..c6521debe3f --- /dev/null +++ b/core/src/com/cloud/agent/api/CreateVolumeFromSnapshotAnswer.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +public class CreateVolumeFromSnapshotAnswer extends Answer { + private String vdiUUID; + + protected CreateVolumeFromSnapshotAnswer() { + + } + + public CreateVolumeFromSnapshotAnswer(CreateVolumeFromSnapshotCommand cmd, boolean success, String result, String vdiUUID) { + super(cmd, success, result); + this.vdiUUID = vdiUUID; + } + + /** + * @return the vdi + */ + public String getVdi() { + return vdiUUID; + } +} diff --git a/core/src/com/cloud/agent/api/CreateVolumeFromSnapshotCommand.java b/core/src/com/cloud/agent/api/CreateVolumeFromSnapshotCommand.java new file mode 100644 index 00000000000..2894c76d2c9 --- /dev/null +++ b/core/src/com/cloud/agent/api/CreateVolumeFromSnapshotCommand.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +/** + * This class encapsulates the information required for creating a new Volume from the backup of a snapshot. + * This currently assumes that both primary and secondary storage are mounted on the XenServer. + */ +public class CreateVolumeFromSnapshotCommand extends SnapshotCommand { + private String templatePath; + + protected CreateVolumeFromSnapshotCommand() { + + } + + /** + * Given the UUID of a backed up snapshot VHD file on the secondary storage, the execute of this command does + * 1) Get the parent chain of this VHD all the way up to the root, say VHDList + * 2) Copy all the files in the VHDlist to some temp location + * 3) Coalesce all the VHDs to one VHD which contains all the data of the volume. This invokes the DeletePreviousBackupCommand for each VHD + * 4) Rename the UUID of this VHD + * 5) Move this VHD to primary storage + * 6) Rename the UUID of this VHD to a new one + * 7) Introduce a VDI with this new VHD and return it. + * @param primaryStoragePoolNameLabel The primary storage Pool + * @param secondaryStoragePoolURL This is what shows up in the UI when you click on Secondary storage. + * In the code, it is present as: In the vmops.host_details table, there is a field mount.parent. This is the value of that field + * If you have better ideas on how to get it, you are welcome. + * @param backedUpSnapshotUuid This is the UUID of the vhd file corresponding to the snapshot id from which the data has to be restored. + * It may not be the UUID of the base copy of the snapshot, if no data was written since last snapshot. + * @param templatePath The install path of the template VHD on the secondary, if this a root volume + */ + public CreateVolumeFromSnapshotCommand(String primaryStoragePoolNameLabel, + String secondaryStoragePoolURL, + Long dcId, + Long accountId, + Long volumeId, + String backedUpSnapshotUuid, + String backedUpSnapshotName, + String templatePath) + { + super(primaryStoragePoolNameLabel, secondaryStoragePoolURL, backedUpSnapshotUuid, backedUpSnapshotName, dcId, accountId, volumeId); + this.templatePath = templatePath; + } + + /** + * @return the templatePath + */ + public String getTemplatePath() { + return templatePath; + } + +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/CreateZoneVlanAnswer.java b/core/src/com/cloud/agent/api/CreateZoneVlanAnswer.java new file mode 100755 index 00000000000..4a2353e31c3 --- /dev/null +++ b/core/src/com/cloud/agent/api/CreateZoneVlanAnswer.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +public class CreateZoneVlanAnswer extends Answer{ + protected CreateZoneVlanAnswer() { + } + + public CreateZoneVlanAnswer(CreateZoneVlanCommand cmd) { + super(cmd); + } + + public CreateZoneVlanAnswer(CreateZoneVlanCommand cmd, String details) { + super(cmd, false, details); + } + +} diff --git a/core/src/com/cloud/agent/api/CreateZoneVlanCommand.java b/core/src/com/cloud/agent/api/CreateZoneVlanCommand.java new file mode 100755 index 00000000000..81828387370 --- /dev/null +++ b/core/src/com/cloud/agent/api/CreateZoneVlanCommand.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.vm.DomainRouter; +import com.cloud.vm.DomainRouterVO; + + +public class CreateZoneVlanCommand extends Command { + + DomainRouterVO router; + + protected CreateZoneVlanCommand() { + super(); + } + + @Override + public boolean executeInSequence() { + return true; + } + + public CreateZoneVlanCommand(DomainRouterVO router) { + this.router = router; + } + + public DomainRouter getRouter() { + return router; + } + +} diff --git a/core/src/com/cloud/agent/api/CronCommand.java b/core/src/com/cloud/agent/api/CronCommand.java new file mode 100755 index 00000000000..03e7ce894da --- /dev/null +++ b/core/src/com/cloud/agent/api/CronCommand.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public interface CronCommand { + /** + * @return interval at which to run the command in seconds. + */ + public int getInterval(); +} diff --git a/core/src/com/cloud/agent/api/DeleteSnapshotBackupAnswer.java b/core/src/com/cloud/agent/api/DeleteSnapshotBackupAnswer.java new file mode 100644 index 00000000000..611ef669c18 --- /dev/null +++ b/core/src/com/cloud/agent/api/DeleteSnapshotBackupAnswer.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +public class DeleteSnapshotBackupAnswer extends Answer { + + protected DeleteSnapshotBackupAnswer() { + + } + + public DeleteSnapshotBackupAnswer(DeleteSnapshotBackupCommand cmd, boolean success, String details) { + super(cmd, success, details); + } + + + +} diff --git a/core/src/com/cloud/agent/api/DeleteSnapshotBackupCommand.java b/core/src/com/cloud/agent/api/DeleteSnapshotBackupCommand.java new file mode 100644 index 00000000000..600477bd7ec --- /dev/null +++ b/core/src/com/cloud/agent/api/DeleteSnapshotBackupCommand.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +/** + * This command encapsulates a primitive operation which enables coalescing the backed up VHD snapshots on the secondary server + * This currently assumes that the secondary storage are mounted on the XenServer. + */ +public class DeleteSnapshotBackupCommand extends SnapshotCommand { + private String childUUID; + + protected DeleteSnapshotBackupCommand() { + + } + + /** + * Given 2 VHD files on the secondary storage which are linked in a parent chain as follows: + * backupUUID = parent(childUUID) + * It gets another VHD + * previousBackupVHD = parent(backupUUID) + * + * And + * 1) it coalesces backupUuid into its parent. + * 2) It deletes the VHD file corresponding to backupUuid + * 3) It sets the parent VHD of childUUID to that of previousBackupUuid + * + * It takes care of the cases when + * 1) childUUID is null. - Step 3 is not done. + * 2) previousBackupUUID is null + * - Merge childUUID into its parent backupUUID + * - Set the UUID of the resultant VHD to childUUID + * - Essentially we are deleting the oldest VHD file and setting the current oldest VHD to childUUID + * + * @param volumeName The name of the volume whose snapshot was taken (something like i-3-SV-ROOT) + * @param secondaryStoragePoolURL This is what shows up in the UI when you click on Secondary storage. + * In the code, it is present as: In the vmops.host_details table, there is a field mount.parent. This is the value of that field + * If you have better ideas on how to get it, you are welcome. + * @param backupUUID The VHD which has to be deleted + * @param childUUID The child VHD file of the backup whose parent is reset to its grandparent. + */ + public DeleteSnapshotBackupCommand(String primaryStoragePoolNameLabel, + String secondaryStoragePoolURL, + Long dcId, + Long accountId, + Long volumeId, + String backupUUID, + String backupName, + String childUUID) + { + super(primaryStoragePoolNameLabel, secondaryStoragePoolURL, backupUUID, backupName, dcId, accountId, volumeId); + this.childUUID = childUUID; + } + + /** + * @return the childUUID + */ + public String getChildUUID() { + return childUUID; + } + +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/DeleteSnapshotsDirCommand.java b/core/src/com/cloud/agent/api/DeleteSnapshotsDirCommand.java new file mode 100644 index 00000000000..2073019e61e --- /dev/null +++ b/core/src/com/cloud/agent/api/DeleteSnapshotsDirCommand.java @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +/** + * This command encapsulates a primitive operation which enables coalescing the backed up VHD snapshots on the secondary server + * This currently assumes that the secondary storage are mounted on the XenServer. + */ +public class DeleteSnapshotsDirCommand extends SnapshotCommand { + + protected DeleteSnapshotsDirCommand() { + + } + + /** + * Given 2 VHD files on the secondary storage which are linked in a parent chain as follows: + * backupUUID = parent(childUUID) + * It gets another VHD + * previousBackupVHD = parent(backupUUID) + * + * And + * 1) it coalesces backupUuid into its parent. + * 2) It deletes the VHD file corresponding to backupUuid + * 3) It sets the parent VHD of childUUID to that of previousBackupUuid + * + * It takes care of the cases when + * 1) childUUID is null. - Step 3 is not done. + * 2) previousBackupUUID is null + * - Merge childUUID into its parent backupUUID + * - Set the UUID of the resultant VHD to childUUID + * - Essentially we are deleting the oldest VHD file and setting the current oldest VHD to childUUID + * + * @param volumeName The name of the volume whose snapshot was taken (something like i-3-SV-ROOT) + * @param secondaryStoragePoolURL This is what shows up in the UI when you click on Secondary storage. + * In the code, it is present as: In the vmops.host_details table, there is a field mount.parent. This is the value of that field + * If you have better ideas on how to get it, you are welcome. + * @param backupUUID The VHD which has to be deleted + * @param childUUID The child VHD file of the backup whose parent is reset to its grandparent. + */ + public DeleteSnapshotsDirCommand(String primaryStoragePoolNameLabel, + String secondaryStoragePoolURL, + Long dcId, + Long accountId, + Long volumeId, + String snapshotUUID, + String snapshotName) + { + super(primaryStoragePoolNameLabel, secondaryStoragePoolURL, snapshotUUID, snapshotName, dcId, accountId, volumeId); + } + +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/DeleteStoragePoolCommand.java b/core/src/com/cloud/agent/api/DeleteStoragePoolCommand.java new file mode 100644 index 00000000000..98ad6beb403 --- /dev/null +++ b/core/src/com/cloud/agent/api/DeleteStoragePoolCommand.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +import java.io.File; +import java.util.UUID; + +import com.cloud.storage.StoragePoolVO; + +public class DeleteStoragePoolCommand extends Command { + + StoragePoolVO pool; + public static final String LOCAL_PATH_PREFIX="/mnt/"; + String localPath; + + + public DeleteStoragePoolCommand() { + + } + + public DeleteStoragePoolCommand(StoragePoolVO pool, String localPath) { + this.pool = pool; + this.localPath = localPath; + } + + public DeleteStoragePoolCommand(StoragePoolVO pool) { + this(pool, LOCAL_PATH_PREFIX + File.separator + UUID.nameUUIDFromBytes((pool.getHostAddress() + pool.getPath()).getBytes())); + } + + public StoragePoolVO getPool() { + return pool; + } + + public void setPool(StoragePoolVO pool) { + this.pool = pool; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public String getLocalPath() { + return localPath; + } + +} diff --git a/core/src/com/cloud/agent/api/FenceAnswer.java b/core/src/com/cloud/agent/api/FenceAnswer.java new file mode 100644 index 00000000000..34e7478dbd1 --- /dev/null +++ b/core/src/com/cloud/agent/api/FenceAnswer.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class FenceAnswer extends Answer { + public FenceAnswer() { + super(); + } + + public FenceAnswer(FenceCommand cmd) { + super(cmd, true, null); + } + + public FenceAnswer(FenceCommand cmd, String details) { + super(cmd, true, details); + } + + public FenceAnswer(FenceCommand cmd, boolean result, String details) { + super(cmd, result, details); + } + +} diff --git a/core/src/com/cloud/agent/api/FenceCommand.java b/core/src/com/cloud/agent/api/FenceCommand.java new file mode 100644 index 00000000000..3b47909ce87 --- /dev/null +++ b/core/src/com/cloud/agent/api/FenceCommand.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.host.HostVO; +import com.cloud.vm.VMInstanceVO; + +public class FenceCommand extends Command { + + public FenceCommand() { + super(); + } + + String vmName; + String hostGuid; + String hostIp; + + public FenceCommand(VMInstanceVO vm, HostVO host) { + super(); + vmName = vm.getInstanceName(); + hostGuid = host.getGuid(); + hostIp = host.getPrivateIpAddress(); + } + + public String getVmName() { + return vmName; + } + + public String getHostGuid() { + return hostGuid; + } + + public String getHostIp() { + return hostIp; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/GetFileStatsAnswer.java b/core/src/com/cloud/agent/api/GetFileStatsAnswer.java new file mode 100755 index 00000000000..9b8abd6ea6a --- /dev/null +++ b/core/src/com/cloud/agent/api/GetFileStatsAnswer.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.storage.VolumeStats; + +/** + * @author ahuang + * + */ +public class GetFileStatsAnswer extends Answer implements VolumeStats { + long size; + protected GetFileStatsAnswer() { + } + + public GetFileStatsAnswer(GetFileStatsCommand cmd, long value) { + super(cmd); + size = value; + } + + public long getBytesUsed() { + return size; + } +} diff --git a/core/src/com/cloud/agent/api/GetFileStatsCommand.java b/core/src/com/cloud/agent/api/GetFileStatsCommand.java new file mode 100755 index 00000000000..97599447092 --- /dev/null +++ b/core/src/com/cloud/agent/api/GetFileStatsCommand.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.storage.Volume; + +public class GetFileStatsCommand extends Command { + protected GetFileStatsCommand() { + } + + String paths; + + public GetFileStatsCommand(Volume volume) { + paths = volume.getPath(); + } + + public String getPaths() { + return paths; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/GetHostStatsAnswer.java b/core/src/com/cloud/agent/api/GetHostStatsAnswer.java new file mode 100755 index 00000000000..c209ac84047 --- /dev/null +++ b/core/src/com/cloud/agent/api/GetHostStatsAnswer.java @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.HashMap; + +import com.cloud.host.HostStats; + +/** + * @author ajoshi + * + */ +public class GetHostStatsAnswer extends Answer implements HostStats { + + HostStatsEntry hostStats; + + double cpuUtilization; + double networkReadKBs; + double networkWriteKBs; + int numCPUs; + String entityType; + double totalMemoryKBs; + double freeMemoryKBs; + double xapiMemoryUsageKBs; + double averageLoad; + + protected GetHostStatsAnswer() { + } + + public GetHostStatsAnswer(GetHostStatsCommand cmd, HostStatsEntry hostStatistics) { + super(cmd); + this.hostStats = hostStatistics; + } + + public GetHostStatsAnswer(GetHostStatsCommand cmd, double cpuUtilization, double freeMemoryKBs, double totalMemoryKBs, double networkReadKBs, + double networkWriteKBs, String entityType, double xapiMemoryUsageKBs, double averageLoad, int numCPUs) { + super(cmd); + + this.cpuUtilization = cpuUtilization; + this.freeMemoryKBs = freeMemoryKBs; + this.totalMemoryKBs = totalMemoryKBs; + this.networkReadKBs = networkReadKBs; + this.networkWriteKBs = networkWriteKBs; + this.entityType = entityType; + this.xapiMemoryUsageKBs = xapiMemoryUsageKBs; + this.numCPUs = numCPUs; + } + + @Override + public double getUsedMemory() { + return (totalMemoryKBs - freeMemoryKBs); + } + + @Override + public double getFreeMemoryKBs() { + return freeMemoryKBs; + } + + @Override + public double getTotalMemoryKBs() { + return totalMemoryKBs; + } + + @Override + public double getCpuUtilization() { + return cpuUtilization; + } + + @Override + public double getNetworkReadKBs() { + return networkReadKBs; + } + + @Override + public double getNetworkWriteKBs() { + return networkWriteKBs; + } + + @Override + public double getAverageLoad() { + return averageLoad; + } + + @Override + public String getEntityType() { + return entityType; + } + + @Override + public double getXapiMemoryUsageKBs() { + return xapiMemoryUsageKBs; + } + + @Override + public int getNumCpus(){ + return numCPUs; + } + + @Override + public HostStats getHostStats() { + return hostStats; + } +} diff --git a/core/src/com/cloud/agent/api/GetHostStatsCommand.java b/core/src/com/cloud/agent/api/GetHostStatsCommand.java new file mode 100755 index 00000000000..05ffe4a0652 --- /dev/null +++ b/core/src/com/cloud/agent/api/GetHostStatsCommand.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.List; + +public class GetHostStatsCommand extends Command +{ + String hostGuid; + String hostName; + long hostId; + + protected GetHostStatsCommand() { + } + + public GetHostStatsCommand(String hostGuid, String hostName, long hostId) { + this.hostGuid = hostGuid; + this.hostName = hostName; + this.hostId = hostId; + } + + public String getHostGuid(){ + return this.hostGuid; + } + + public String getHostName(){ + return this.hostName; + } + + public long getHostId(){ + return this.hostId; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/GetStorageStatsAnswer.java b/core/src/com/cloud/agent/api/GetStorageStatsAnswer.java new file mode 100755 index 00000000000..f7eba8e7aa8 --- /dev/null +++ b/core/src/com/cloud/agent/api/GetStorageStatsAnswer.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.storage.StorageStats; + +public class GetStorageStatsAnswer extends Answer implements StorageStats { + protected GetStorageStatsAnswer() { + } + + protected long used; + + protected long capacity; + + public long getByteUsed() { + return used; + } + + public long getCapacityBytes() { + return capacity; + } + + + public GetStorageStatsAnswer(GetStorageStatsCommand cmd, long capacity, long used) { + super(cmd, true, null); + this.capacity = capacity; + this.used = used; + } + + public GetStorageStatsAnswer(GetStorageStatsCommand cmd, String details) { + super(cmd, false, details); + } + +} diff --git a/core/src/com/cloud/agent/api/GetStorageStatsCommand.java b/core/src/com/cloud/agent/api/GetStorageStatsCommand.java new file mode 100755 index 00000000000..e08ed9bf56d --- /dev/null +++ b/core/src/com/cloud/agent/api/GetStorageStatsCommand.java @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.storage.Storage.StoragePoolType; + +public class GetStorageStatsCommand extends Command { + private String id; + private String localPath; + private StoragePoolType pooltype; + + + public GetStorageStatsCommand() { + } + + public StoragePoolType getPooltype() { + return pooltype; + } + + public void setPooltype(StoragePoolType pooltype) { + this.pooltype = pooltype; + } + + public GetStorageStatsCommand(String id) { + this.id = id; + } + + public GetStorageStatsCommand(String id, StoragePoolType pooltype) { + this.id = id; + this.pooltype = pooltype; + } + + public GetStorageStatsCommand(String id, StoragePoolType pooltype, String localPath) { + this.id = id; + this.pooltype = pooltype; + this.localPath = localPath; + } + + public String getStorageId() { + return this.id; + } + + public String getLocalPath() { + return this.localPath; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/GetVmStatsAnswer.java b/core/src/com/cloud/agent/api/GetVmStatsAnswer.java new file mode 100755 index 00000000000..0255a43884d --- /dev/null +++ b/core/src/com/cloud/agent/api/GetVmStatsAnswer.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +import java.util.HashMap; + +public class GetVmStatsAnswer extends Answer { + + HashMap vmStatsMap; + + public GetVmStatsAnswer(GetVmStatsCommand cmd, HashMap vmStatsMap) { + super(cmd); + this.vmStatsMap = vmStatsMap; + } + + public HashMap getVmStatsMap() { + return vmStatsMap; + } + + protected GetVmStatsAnswer() { + //no-args constructor for json serialization-deserialization + } +} diff --git a/core/src/com/cloud/agent/api/GetVmStatsCommand.java b/core/src/com/cloud/agent/api/GetVmStatsCommand.java new file mode 100755 index 00000000000..498c119c92a --- /dev/null +++ b/core/src/com/cloud/agent/api/GetVmStatsCommand.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.List; + +import com.cloud.vm.VirtualMachine; + +public class GetVmStatsCommand extends Command { + List vmNames; + String hostGuid; + String hostName; + + protected GetVmStatsCommand() { + } + + public GetVmStatsCommand(List vmNames, String hostGuid, String hostName) { + this.vmNames = vmNames; + this.hostGuid = hostGuid; + this.hostName = hostName; + } + + public List getVmNames() { + return vmNames; + } + + public String getHostGuid(){ + return this.hostGuid; + } + + public String getHostName(){ + return this.hostName; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/GetVncPortAnswer.java b/core/src/com/cloud/agent/api/GetVncPortAnswer.java new file mode 100644 index 00000000000..7a1f359a999 --- /dev/null +++ b/core/src/com/cloud/agent/api/GetVncPortAnswer.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class GetVncPortAnswer extends Answer { + int port; + + protected GetVncPortAnswer() { + } + + public GetVncPortAnswer(GetVncPortCommand cmd, int port) { + super(cmd, true, null); + this.port = port; + } + + public GetVncPortAnswer(GetVncPortCommand cmd, String details) { + super(cmd, false, details); + } + + public int getPort() { + return port; + } +} diff --git a/core/src/com/cloud/agent/api/GetVncPortCommand.java b/core/src/com/cloud/agent/api/GetVncPortCommand.java new file mode 100644 index 00000000000..196b8f238a8 --- /dev/null +++ b/core/src/com/cloud/agent/api/GetVncPortCommand.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class GetVncPortCommand extends Command { + long id; + String name; + + public GetVncPortCommand() { + } + + public GetVncPortCommand(long id, String name) { + this.id = id; + this.name = name; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public String getName() { + return name; + } + + public long getId() { + return id; + } +} diff --git a/core/src/com/cloud/agent/api/HostStatsEntry.java b/core/src/com/cloud/agent/api/HostStatsEntry.java new file mode 100644 index 00000000000..65248aaa120 --- /dev/null +++ b/core/src/com/cloud/agent/api/HostStatsEntry.java @@ -0,0 +1,148 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.host.HostStats; + +/** + * @author ajoshi + * + */ +public class HostStatsEntry implements HostStats { + + long hostId; + double cpuUtilization; + double networkReadKBs; + double networkWriteKBs; + int numCpus; + String entityType; + double totalMemoryKBs; + double freeMemoryKBs; + double xapiMemoryUsageKBs; + double averageLoad; + + public HostStatsEntry() { + } + + public HostStatsEntry(long hostId,double cpuUtilization, double networkReadKBs, double networkWriteKBs, int numCPUs, String entityType, + double totalMemoryKBs, double freeMemoryKBs, double xapiMemoryUsageKBs, double averageLoad) + { + this.cpuUtilization = cpuUtilization; + this.networkReadKBs = networkReadKBs; + this.networkWriteKBs = networkWriteKBs; + this.numCpus = numCPUs; + this.entityType = entityType; + this.totalMemoryKBs = totalMemoryKBs; + this.freeMemoryKBs = freeMemoryKBs; + this.xapiMemoryUsageKBs = xapiMemoryUsageKBs; + this.averageLoad = averageLoad; + this.hostId = hostId; + } + + @Override + public double getAverageLoad() { + return averageLoad; + } + + public void setAverageLoad(double averageLoad) { + this.averageLoad = averageLoad; + } + + @Override + public double getNetworkReadKBs() { + return networkReadKBs; + } + + public void setNetworkReadKBs(double networkReadKBs) { + this.networkReadKBs = networkReadKBs; + } + + @Override + public double getNetworkWriteKBs() { + return networkWriteKBs; + } + + public void setNetworkWriteKBs(double networkWriteKBs) { + this.networkWriteKBs = networkWriteKBs; + } + + public void setNumCpus(int numCpus) { + this.numCpus = numCpus; + } + + @Override + public String getEntityType(){ + return this.entityType; + } + + public void setEntityType(String entityType){ + this.entityType = entityType; + } + + @Override + public double getTotalMemoryKBs(){ + return this.totalMemoryKBs; + } + + public void setTotalMemoryKBs(double totalMemoryKBs){ + this.totalMemoryKBs = totalMemoryKBs; + } + + @Override + public double getFreeMemoryKBs(){ + return this.freeMemoryKBs; + } + + public void setFreeMemoryKBs(double freeMemoryKBs){ + this.freeMemoryKBs = freeMemoryKBs; + } + + @Override + public double getXapiMemoryUsageKBs(){ + return this.xapiMemoryUsageKBs; + } + + public void setXapiMemoryUsageKBs(double xapiMemoryUsageKBs){ + this.xapiMemoryUsageKBs = xapiMemoryUsageKBs; + } + + @Override + public double getCpuUtilization() { + return this.cpuUtilization; + } + + public void setCpuUtilization(double cpuUtilization) { + this.cpuUtilization = cpuUtilization; + } + + @Override + public double getUsedMemory() { + return (totalMemoryKBs-freeMemoryKBs); + } + + @Override + public int getNumCpus() { + return numCpus; + } + + @Override + public HostStats getHostStats() { + // TODO Auto-generated method stub + return null; + } +} diff --git a/core/src/com/cloud/agent/api/MaintainAnswer.java b/core/src/com/cloud/agent/api/MaintainAnswer.java new file mode 100644 index 00000000000..48ad96d3dd1 --- /dev/null +++ b/core/src/com/cloud/agent/api/MaintainAnswer.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class MaintainAnswer extends Answer { + public MaintainAnswer() { + } + + public MaintainAnswer(MaintainCommand cmd) { + this(cmd, true, null); + } + + public MaintainAnswer(MaintainCommand cmd, String details) { + this(cmd, true, details); + } + + public MaintainAnswer(MaintainCommand cmd, boolean result, String details) { + super(cmd, result, details); + } +} diff --git a/core/src/com/cloud/agent/api/MaintainCommand.java b/core/src/com/cloud/agent/api/MaintainCommand.java new file mode 100644 index 00000000000..040309adaa7 --- /dev/null +++ b/core/src/com/cloud/agent/api/MaintainCommand.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class MaintainCommand extends Command { + + public MaintainCommand() { + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/com/cloud/agent/api/ManageSnapshotAnswer.java b/core/src/com/cloud/agent/api/ManageSnapshotAnswer.java new file mode 100644 index 00000000000..8e357098fbb --- /dev/null +++ b/core/src/com/cloud/agent/api/ManageSnapshotAnswer.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + + +public class ManageSnapshotAnswer extends Answer { + // For create Snapshot + private String _snapshotPath; + + public ManageSnapshotAnswer() {} + + public ManageSnapshotAnswer(Command cmd, boolean success, String result) { + super(cmd, success, result); + } + + // For XenServer + public ManageSnapshotAnswer(ManageSnapshotCommand cmd, long snapshotId, String snapshotPath, boolean success, String result) { + super(cmd, success, result); + _snapshotPath = snapshotPath; + } + + public String getSnapshotPath() { + return _snapshotPath; + } + +} diff --git a/core/src/com/cloud/agent/api/ManageSnapshotCommand.java b/core/src/com/cloud/agent/api/ManageSnapshotCommand.java new file mode 100644 index 00000000000..57497bc478b --- /dev/null +++ b/core/src/com/cloud/agent/api/ManageSnapshotCommand.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + + +public class ManageSnapshotCommand extends Command { + // XXX: Should be an enum + // XXX: Anyway there is something called inheritance in Java + public static String CREATE_SNAPSHOT = "-c"; + public static String DESTROY_SNAPSHOT = "-d"; + + private String _commandSwitch; + + // Information about the volume that the snapshot is based on + private String _volumePath = null; + + // Information about the snapshot + private String _snapshotPath = null; + private String _snapshotName = null; + private long _snapshotId; + private String _vmName = null; + + public ManageSnapshotCommand() {} + + public ManageSnapshotCommand(String commandSwitch, long snapshotId, String path, String snapshotName, String vmName) { + _commandSwitch = commandSwitch; + if (commandSwitch.equals(ManageSnapshotCommand.CREATE_SNAPSHOT)) { + _volumePath = path; + } + else if (commandSwitch.equals(ManageSnapshotCommand.DESTROY_SNAPSHOT)) { + _snapshotPath = path; + } + _snapshotName = snapshotName; + _snapshotId = snapshotId; + _vmName = vmName; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public String getCommandSwitch() { + return _commandSwitch; + } + + public String getVolumePath() { + return _volumePath; + } + + public String getSnapshotPath() { + return _snapshotPath; + } + + public String getSnapshotName() { + return _snapshotName; + } + + public long getSnapshotId() { + return _snapshotId; + } + + public String getVmName() { + return _vmName; + } + +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/MigrateAnswer.java b/core/src/com/cloud/agent/api/MigrateAnswer.java new file mode 100644 index 00000000000..42610d7e6d0 --- /dev/null +++ b/core/src/com/cloud/agent/api/MigrateAnswer.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class MigrateAnswer extends Answer { + Integer vncPort = null; + + protected MigrateAnswer() { + } + + public MigrateAnswer(MigrateCommand cmd, boolean result, String detail, Integer vncPort) { + super(cmd, result, detail); + this.vncPort = vncPort; + } + + public Integer getVncPort() { + return vncPort; + } +} diff --git a/core/src/com/cloud/agent/api/MigrateCommand.java b/core/src/com/cloud/agent/api/MigrateCommand.java new file mode 100644 index 00000000000..502647dd8e7 --- /dev/null +++ b/core/src/com/cloud/agent/api/MigrateCommand.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class MigrateCommand extends Command { + String vmName; + String destIp; + boolean isWindows; + + + protected MigrateCommand() { + } + + public MigrateCommand(String vmName, String destIp, boolean isWindows) { + this.vmName = vmName; + this.destIp = destIp; + this.isWindows = isWindows; + } + + public boolean isWindows() { + return isWindows; + } + + public String getDestinationIp() { + return destIp; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/com/cloud/agent/api/MirrorAnswer.java b/core/src/com/cloud/agent/api/MirrorAnswer.java new file mode 100644 index 00000000000..eb627ab6a82 --- /dev/null +++ b/core/src/com/cloud/agent/api/MirrorAnswer.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.ArrayList; +import java.util.List; + +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; + + +/** + * @author chiradeep + * + */ +public class MirrorAnswer extends Answer { + public enum MirrorState { + NOT_MIRRORED, + ACTIVE, + DEGRADED, + FAILED + } + + public enum DiskState { + ACTIVE, + REBUILD, + FAILED + } + + public class MirrorInfo { + public Volume.VolumeType volType; + public VolumeVO vol1; + public VolumeVO vol2; + public DiskState disk1State; + public DiskState disk2State; + public MirrorState mirrorState; + public int rebuildPct; + } + + String vmName; + //List mirrorInfo = new ArrayList(); + String error; + + + protected MirrorAnswer() { + } + + + public MirrorAnswer(MirrorCommand cmd, String vmName, String err) { + super(cmd, false, err); + this.vmName = vmName; + } + + public MirrorAnswer(MirrorCommand cmd, String vmName){ + super(cmd, true, null); + this.vmName = vmName; + } + + public String getVmName() { + return vmName; + } + + + public void setVmName(String vmName) { + this.vmName = vmName; + } + + + +} diff --git a/core/src/com/cloud/agent/api/MirrorCommand.java b/core/src/com/cloud/agent/api/MirrorCommand.java new file mode 100644 index 00000000000..c7d10f4abe5 --- /dev/null +++ b/core/src/com/cloud/agent/api/MirrorCommand.java @@ -0,0 +1,104 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +import java.util.List; + +import com.cloud.storage.VolumeVO; + +public class MirrorCommand extends Command { + + protected String vmName; + protected String removeHost; + protected String addHost; + protected List removeVols; + protected List addVols; + + + public MirrorCommand(String vmName, String removeHost, String addHost, + List removeVols, List addVols) { + super(); + this.vmName = vmName; + this.removeHost = removeHost; + this.addHost = addHost; + this.removeVols = removeVols; + this.addVols = addVols; + } + + protected MirrorCommand() { + //satisfies gson + } + + public String getVmName() { + return vmName; + } + + + public void setVmName(String vmName) { + this.vmName = vmName; + } + + + public String getRemoveHost() { + return removeHost; + } + + + public void setRemoveHost(String removeHost) { + this.removeHost = removeHost; + } + + + public String getAddHost() { + return addHost; + } + + + public void setAddHost(String addHost) { + this.addHost = addHost; + } + + + public List getRemoveVols() { + return removeVols; + } + + + public void setRemoveVols(List removeVols) { + this.removeVols = removeVols; + } + + + public List getAddVols() { + return addVols; + } + + + public void setAddVols(List addVols) { + this.addVols = addVols; + } + + + @Override + public boolean executeInSequence() { + return true; + } + + +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/ModifySshKeysCommand.java b/core/src/com/cloud/agent/api/ModifySshKeysCommand.java new file mode 100644 index 00000000000..3e10897af63 --- /dev/null +++ b/core/src/com/cloud/agent/api/ModifySshKeysCommand.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class ModifySshKeysCommand extends Command { + private String _pubKey; + private String _prvKey; + public ModifySshKeysCommand() { + + } + public ModifySshKeysCommand(String pubKey, String prvKey) { + _pubKey = pubKey; + _prvKey = prvKey; + } + public String getPubKey() { + return _pubKey; + } + public String getPrvKey() { + return _prvKey; + } + @Override + public boolean executeInSequence() { + // TODO Auto-generated method stub + return false; + } + +} diff --git a/core/src/com/cloud/agent/api/ModifyStoragePoolAnswer.java b/core/src/com/cloud/agent/api/ModifyStoragePoolAnswer.java new file mode 100644 index 00000000000..33609203a68 --- /dev/null +++ b/core/src/com/cloud/agent/api/ModifyStoragePoolAnswer.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.Map; + +import com.cloud.storage.template.TemplateInfo; + +/** + * @author chiradeep + * + */ +public class ModifyStoragePoolAnswer extends Answer { + StoragePoolInfo poolInfo; + Map templateInfo; + + protected ModifyStoragePoolAnswer() { + } + + public ModifyStoragePoolAnswer(ModifyStoragePoolCommand cmd, long capacityBytes, long availableBytes, Map tInfo) { + super(cmd); + this.result = true; + this.poolInfo = new StoragePoolInfo(null, cmd.getPool().getName(), + cmd.getPool().getHostAddress(), cmd.getPool().getPath(), cmd.getLocalPath(), + cmd.getPool().getPoolType(), capacityBytes, availableBytes ); + + this.templateInfo = tInfo; + } + + public StoragePoolInfo getPoolInfo() { + return poolInfo; + } + + public void setPoolInfo(StoragePoolInfo poolInfo) { + this.poolInfo = poolInfo; + } + + + public Map getTemplateInfo() { + return templateInfo; + } + + public void setTemplateInfo(Map templateInfo) { + this.templateInfo = templateInfo; + } + +} diff --git a/core/src/com/cloud/agent/api/ModifyStoragePoolCommand.java b/core/src/com/cloud/agent/api/ModifyStoragePoolCommand.java new file mode 100644 index 00000000000..5941b1b5471 --- /dev/null +++ b/core/src/com/cloud/agent/api/ModifyStoragePoolCommand.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +import java.io.File; +import java.util.UUID; + +import com.cloud.storage.StoragePoolVO; + +public class ModifyStoragePoolCommand extends Command { + + boolean add; + StoragePoolVO pool; + String localPath; + String [] options; + public static final String LOCAL_PATH_PREFIX="/mnt/"; + + + public ModifyStoragePoolCommand() { + + } + + public ModifyStoragePoolCommand(boolean add, StoragePoolVO pool, String localPath) { + this.add = add; + this.pool = pool; + this.localPath = localPath; + + } + + public ModifyStoragePoolCommand(boolean add, StoragePoolVO pool) { + this(add, pool, LOCAL_PATH_PREFIX + File.separator + UUID.nameUUIDFromBytes((pool.getHostAddress() + pool.getPath()).getBytes())); + } + + public StoragePoolVO getPool() { + return pool; + } + + public void setPool(StoragePoolVO pool) { + this.pool = pool; + } + + public boolean getAdd() { + return add; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public String getLocalPath() { + return localPath; + } + + public void setOptions(String[] options) { + this.options = options; + } + + +} diff --git a/core/src/com/cloud/agent/api/NetworkIngressRuleAnswer.java b/core/src/com/cloud/agent/api/NetworkIngressRuleAnswer.java new file mode 100644 index 00000000000..53f2365dcef --- /dev/null +++ b/core/src/com/cloud/agent/api/NetworkIngressRuleAnswer.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class NetworkIngressRuleAnswer extends Answer { + Long logSequenceNumber = null; + Long vmId = null; + + protected NetworkIngressRuleAnswer() { + } + + public NetworkIngressRuleAnswer(NetworkIngressRulesCmd cmd) { + super(cmd); + this.logSequenceNumber = cmd.getSeqNum(); + this.vmId = cmd.getVmId(); + } + + public NetworkIngressRuleAnswer(NetworkIngressRulesCmd cmd, boolean result, String detail) { + super(cmd, result, detail); + this.logSequenceNumber = cmd.getSeqNum(); + this.vmId = cmd.getVmId(); + } + + public Long getLogSequenceNumber() { + return logSequenceNumber; + } + + public Long getVmId() { + return vmId; + } + + @Override + public String toString() { + return "[NWGRPRuleAns: vmId=" + vmId + ", seqno=" + logSequenceNumber+"]"; + } + + +} diff --git a/core/src/com/cloud/agent/api/NetworkIngressRulesCmd.java b/core/src/com/cloud/agent/api/NetworkIngressRulesCmd.java new file mode 100644 index 00000000000..9151553038a --- /dev/null +++ b/core/src/com/cloud/agent/api/NetworkIngressRulesCmd.java @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class NetworkIngressRulesCmd extends Command { + public static class IpPortAndProto { + String proto; + int startPort; + int endPort; + String [] allowedCidrs; + + public IpPortAndProto() { } + + public IpPortAndProto(String proto, int startPort, int endPort, + String[] allowedCidrs) { + super(); + this.proto = proto; + this.startPort = startPort; + this.endPort = endPort; + this.allowedCidrs = allowedCidrs; + } + + public String[] getAllowedCidrs() { + return allowedCidrs; + } + + public void setAllowedCidrs(String[] allowedCidrs) { + this.allowedCidrs = allowedCidrs; + } + + public String getProto() { + return proto; + } + + public int getStartPort() { + return startPort; + } + + public int getEndPort() { + return endPort; + } + + } + + + String guestIp; + String vmName; + String guestMac; + String signature; + Long seqNum; + Long vmId; + IpPortAndProto [] ruleSet; + + public NetworkIngressRulesCmd() { + super(); + } + + + public NetworkIngressRulesCmd(String guestIp, String guestMac, String vmName, Long vmId, String signature, Long seqNum, IpPortAndProto[] ruleSet) { + super(); + this.guestIp = guestIp; + this.vmName = vmName; + this.ruleSet = ruleSet; + this.guestMac = guestMac; + this.signature = signature; + this.seqNum = seqNum; + this.vmId = vmId; + } + + + @Override + public boolean executeInSequence() { + return true; + } + + + public IpPortAndProto[] getRuleSet() { + return ruleSet; + } + + + public void setRuleSet(IpPortAndProto[] ruleSet) { + this.ruleSet = ruleSet; + } + + + public String getGuestIp() { + return guestIp; + } + + + public String getVmName() { + return vmName; + } + + public String stringifyRules() { + StringBuilder ruleBuilder = new StringBuilder(); + for (NetworkIngressRulesCmd.IpPortAndProto ipPandP: getRuleSet()) { + ruleBuilder.append(ipPandP.getProto()).append(":").append(ipPandP.getStartPort()).append(":").append(ipPandP.getEndPort()).append(":"); + for (String cidr: ipPandP.getAllowedCidrs()) { + ruleBuilder.append(cidr).append(","); + } + ruleBuilder.append("NEXT"); + ruleBuilder.append(" "); + } + return ruleBuilder.toString(); + } + + public String toJson() { + GsonBuilder gBuilder = new GsonBuilder(); + Gson json = gBuilder.create(); + return json.toJson(this); + } + + public static NetworkIngressRulesCmd fromJson(String jsonString) { + GsonBuilder gBuilder = new GsonBuilder(); + Gson json = gBuilder.create(); + return json.fromJson(jsonString, NetworkIngressRulesCmd.class); + } + + + public String getSignature() { + return signature; + } + + + public String getGuestMac() { + return guestMac; + } + + + public Long getSeqNum() { + return seqNum; + } + + + public Long getVmId() { + return vmId; + } + +} diff --git a/core/src/com/cloud/agent/api/NetworkUsageAnswer.java b/core/src/com/cloud/agent/api/NetworkUsageAnswer.java new file mode 100644 index 00000000000..bf4a4d5a7a9 --- /dev/null +++ b/core/src/com/cloud/agent/api/NetworkUsageAnswer.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class NetworkUsageAnswer extends Answer { + Long bytesSent; + Long bytesReceived; + + protected NetworkUsageAnswer() { + } + + public NetworkUsageAnswer(NetworkUsageCommand cmd, String details, Long bytesSent, Long bytesReceived) { + super(cmd, true, details); + this.bytesReceived = bytesReceived; + this.bytesSent = bytesSent; + } + + + public void setBytesReceived(Long bytesReceived) { + this.bytesReceived = bytesReceived; + } + + public Long getBytesReceived() { + return bytesReceived; + } + + public void setBytesSent(Long bytesSent) { + this.bytesSent = bytesSent; + } + + public Long getBytesSent() { + return bytesSent; + } +} diff --git a/core/src/com/cloud/agent/api/NetworkUsageCommand.java b/core/src/com/cloud/agent/api/NetworkUsageCommand.java new file mode 100644 index 00000000000..c9c88279f6d --- /dev/null +++ b/core/src/com/cloud/agent/api/NetworkUsageCommand.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +public class NetworkUsageCommand extends Command { + private String privateIP; + + protected NetworkUsageCommand() { + + } + + public NetworkUsageCommand(String privateIP) + { + this.privateIP = privateIP; + } + + public String getPrivateIP() { + return privateIP; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean executeInSequence() { + return false; + } + +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/PingAnswer.java b/core/src/com/cloud/agent/api/PingAnswer.java new file mode 100644 index 00000000000..ff9481c5cf0 --- /dev/null +++ b/core/src/com/cloud/agent/api/PingAnswer.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class PingAnswer extends Answer { + private PingCommand _command = null; + + protected PingAnswer() { + } + + public PingAnswer(PingCommand cmd) { + super(cmd); + _command = cmd; + } + + public PingCommand getCommand() { + return _command; + } +} diff --git a/core/src/com/cloud/agent/api/PingCommand.java b/core/src/com/cloud/agent/api/PingCommand.java new file mode 100755 index 00000000000..4f171330b58 --- /dev/null +++ b/core/src/com/cloud/agent/api/PingCommand.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.host.Host; + +public class PingCommand extends Command { + Host.Type hostType; + long hostId; + + protected PingCommand() { + } + + public PingCommand(Host.Type type, long id) { + hostType = type; + hostId = id; + } + + public Host.Type getHostType() { + return hostType; + } + + public long getHostId() { + return hostId; + } + + @Override + public boolean executeInSequence() { + return false; + } + } diff --git a/core/src/com/cloud/agent/api/PingRoutingCommand.java b/core/src/com/cloud/agent/api/PingRoutingCommand.java new file mode 100755 index 00000000000..16e889649b0 --- /dev/null +++ b/core/src/com/cloud/agent/api/PingRoutingCommand.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.Map; + +import com.cloud.host.Host; +import com.cloud.vm.State; + +public class PingRoutingCommand extends PingCommand { + Map newStates; + boolean _gatewayAccessible = true; + boolean _vnetAccessible = true; + + protected PingRoutingCommand() { + } + + public PingRoutingCommand(Host.Type type, long id, Map states) { + super(type, id); + this.newStates = states; + } + + public Map getNewStates() { + return newStates; + } + + public boolean isGatewayAccessible() { + return _gatewayAccessible; + } + public void setGatewayAccessible(boolean gatewayAccessible) { + _gatewayAccessible = gatewayAccessible; + } + + public boolean isVnetAccessible() { + return _vnetAccessible; + } + public void setVnetAccessible(boolean vnetAccessible) { + _vnetAccessible = vnetAccessible; + } +} diff --git a/core/src/com/cloud/agent/api/PingRoutingWithNwGroupsCommand.java b/core/src/com/cloud/agent/api/PingRoutingWithNwGroupsCommand.java new file mode 100644 index 00000000000..9b51f1de3de --- /dev/null +++ b/core/src/com/cloud/agent/api/PingRoutingWithNwGroupsCommand.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later +version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.HashMap; +import java.util.Map; + +import com.cloud.host.Host.Type; +import com.cloud.utils.Pair; +import com.cloud.vm.State; + + +public class PingRoutingWithNwGroupsCommand extends PingRoutingCommand { + HashMap> newGroupStates; + + protected PingRoutingWithNwGroupsCommand() { + super(); + } + + public PingRoutingWithNwGroupsCommand(Type type, long id, Map states, HashMap> nwGrpStates) { + super(type, id, states); + newGroupStates = nwGrpStates; + } + + public HashMap> getNewGroupStates() { + return newGroupStates; + } + + public void setNewGroupStates(HashMap> newGroupStates) { + this.newGroupStates = newGroupStates; + } + +} diff --git a/core/src/com/cloud/agent/api/PingStorageCommand.java b/core/src/com/cloud/agent/api/PingStorageCommand.java new file mode 100755 index 00000000000..921cdb5c9bd --- /dev/null +++ b/core/src/com/cloud/agent/api/PingStorageCommand.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.Map; + +import com.cloud.host.Host; + +public class PingStorageCommand extends PingCommand { + Map changes; + + protected PingStorageCommand() { + } + + public PingStorageCommand(Host.Type type, long id, Map changes) { + super(type, id); + this.changes = changes; + } + + public Map getChanges() { + return changes; + } +} diff --git a/core/src/com/cloud/agent/api/PingTestCommand.java b/core/src/com/cloud/agent/api/PingTestCommand.java new file mode 100644 index 00000000000..98970315b07 --- /dev/null +++ b/core/src/com/cloud/agent/api/PingTestCommand.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class PingTestCommand extends Command { + + String _computingHostIp = null; + String _routerIp = null; + String _privateIp = null; + + public PingTestCommand() {} + + public PingTestCommand(String computingHostIp) { + _computingHostIp = computingHostIp; + } + + public PingTestCommand(String routerIp, String privateIp) { + _routerIp = routerIp; + _privateIp = privateIp; + } + + public String getComputingHostIp() { + return _computingHostIp; + } + + public String getRouterIp() { + return _routerIp; + } + + public String getPrivateIp() { + return _privateIp; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/PoolEjectCommand.java b/core/src/com/cloud/agent/api/PoolEjectCommand.java new file mode 100644 index 00000000000..ffe57a39072 --- /dev/null +++ b/core/src/com/cloud/agent/api/PoolEjectCommand.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class PoolEjectCommand extends Command { + private String hostuuid; + + public String getHostuuid() { + return hostuuid; + } + + public void setHostuuid(String hostuuid) { + this.hostuuid = hostuuid; + } + public PoolEjectCommand() { + super(); + } + public PoolEjectCommand(String hostuuid) { + super(); + setHostuuid(hostuuid); + } + + @Override + public boolean executeInSequence() { + return true; + } + +} diff --git a/core/src/com/cloud/agent/api/PrepareForMigrationAnswer.java b/core/src/com/cloud/agent/api/PrepareForMigrationAnswer.java new file mode 100644 index 00000000000..05875489aee --- /dev/null +++ b/core/src/com/cloud/agent/api/PrepareForMigrationAnswer.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class PrepareForMigrationAnswer extends Answer { + protected PrepareForMigrationAnswer() { + } + + public PrepareForMigrationAnswer(PrepareForMigrationCommand cmd, boolean result, String detail) { + super(cmd, result, detail); + } +} diff --git a/core/src/com/cloud/agent/api/PrepareForMigrationCommand.java b/core/src/com/cloud/agent/api/PrepareForMigrationCommand.java new file mode 100644 index 00000000000..8efd4397863 --- /dev/null +++ b/core/src/com/cloud/agent/api/PrepareForMigrationCommand.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.List; +import java.util.Map; + +import com.cloud.storage.VolumeVO; + +public class PrepareForMigrationCommand extends AbstractStartCommand { + String vnet; + + protected PrepareForMigrationCommand() { + } + + public PrepareForMigrationCommand(String vmName, String vnet, String[] storageHosts, List vols, boolean mirrored) { + super(vmName, storageHosts, vols, mirrored); + this.vnet = vnet; + } + + public String getVnet() { + return vnet; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/com/cloud/agent/api/ReadyAnswer.java b/core/src/com/cloud/agent/api/ReadyAnswer.java new file mode 100644 index 00000000000..d03a98e1c9e --- /dev/null +++ b/core/src/com/cloud/agent/api/ReadyAnswer.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class ReadyAnswer extends Answer { + protected ReadyAnswer() { + } + + public ReadyAnswer(ReadyCommand cmd) { + super(cmd, true, null); + } + + public ReadyAnswer(ReadyCommand cmd, String details) { + super(cmd, false, details); + } + +} diff --git a/core/src/com/cloud/agent/api/ReadyCommand.java b/core/src/com/cloud/agent/api/ReadyCommand.java new file mode 100644 index 00000000000..a554fbed90b --- /dev/null +++ b/core/src/com/cloud/agent/api/ReadyCommand.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class ReadyCommand extends Command { + + public ReadyCommand() { + super(); + } + + private Long dcId; + + public ReadyCommand(Long dcId) { + super(); + this.dcId = dcId; + } + + public Long getDataCenterId() { + return dcId; + } + + @Override + public boolean executeInSequence() { + return true; + } + +} diff --git a/core/src/com/cloud/agent/api/RebootAnswer.java b/core/src/com/cloud/agent/api/RebootAnswer.java new file mode 100644 index 00000000000..a16f06f2ceb --- /dev/null +++ b/core/src/com/cloud/agent/api/RebootAnswer.java @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class RebootAnswer extends Answer { + Long bytesSent; + Long bytesReceived; + Integer vncPort; + + protected RebootAnswer() { + } + + public RebootAnswer(RebootCommand cmd, String details, Long bytesSent, Long bytesReceived, Integer vncport) { + super(cmd, true, details); + this.bytesReceived = bytesReceived; + this.bytesSent = bytesSent; + this.vncPort = vncport; + } + + public RebootAnswer(RebootCommand cmd, String details, Long bytesSent, Long bytesReceived) { + super(cmd, true, details); + this.bytesReceived = bytesReceived; + this.bytesSent = bytesSent; + this.vncPort = null; + } + + public RebootAnswer(RebootCommand cmd, String details) { + super(cmd, false, details); + bytesSent = null; + bytesReceived = null; + } + + public void setBytesReceived(Long bytesReceived) { + this.bytesReceived = bytesReceived; + } + + public Long getBytesReceived() { + return bytesReceived; + } + + public void setBytesSent(Long bytesSent) { + this.bytesSent = bytesSent; + } + + public Long getBytesSent() { + return bytesSent; + } + public Integer getVncPort() { + return vncPort; + } +} diff --git a/core/src/com/cloud/agent/api/RebootCommand.java b/core/src/com/cloud/agent/api/RebootCommand.java new file mode 100755 index 00000000000..46431d98932 --- /dev/null +++ b/core/src/com/cloud/agent/api/RebootCommand.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.vm.VirtualMachine; + +/** + * @author ahuang + * + */ +public class RebootCommand extends Command { + String vmName; + + protected RebootCommand() { + } + + public RebootCommand(VirtualMachine vm) { + vmName = vm.getInstanceName(); + } + + public RebootCommand(String vmName) { + this.vmName = vmName; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } + +} diff --git a/core/src/com/cloud/agent/api/RebootRouterCommand.java b/core/src/com/cloud/agent/api/RebootRouterCommand.java new file mode 100644 index 00000000000..6bb046232c1 --- /dev/null +++ b/core/src/com/cloud/agent/api/RebootRouterCommand.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + + +public class RebootRouterCommand extends RebootCommand { + + protected String privateIp; + + protected RebootRouterCommand() { + } + + + public RebootRouterCommand(String vmName, String privateIp) { + super(vmName); + this.privateIp=privateIp; + } + + + public String getPrivateIpAddress() { + return privateIp; + } + +} diff --git a/core/src/com/cloud/agent/api/SecStorageFirewallCfgCommand.java b/core/src/com/cloud/agent/api/SecStorageFirewallCfgCommand.java new file mode 100644 index 00000000000..a22a309f0a7 --- /dev/null +++ b/core/src/com/cloud/agent/api/SecStorageFirewallCfgCommand.java @@ -0,0 +1,125 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import com.cloud.agent.transport.Request; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.annotations.Expose; +import com.google.gson.reflect.TypeToken; + +public class SecStorageFirewallCfgCommand extends Command { + + + + public static class PortConfigListTypeAdaptor implements JsonDeserializer>, JsonSerializer> { + static final GsonBuilder s_gBuilder; + static { + s_gBuilder = new GsonBuilder().excludeFieldsWithoutExposeAnnotation(); + } + + Type listType = new TypeToken>() {}.getType(); + + public PortConfigListTypeAdaptor() { + } + + public JsonElement serialize(List src, Type typeOfSrc, JsonSerializationContext context) { + Gson json = s_gBuilder.create(); + String result = json.toJson(src, listType); + return new JsonPrimitive(result); + } + + public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + String jsonString = json.toString(); + Gson jsonp = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + List pcs = jsonp.fromJson(jsonString, listType); + return pcs; + } + + } + public static class PortConfig { + @Expose boolean add; + @Expose String sourceIp; + @Expose String port; + @Expose String intf; + public PortConfig(String sourceIp, String port, boolean add, String intf) { + this.add = add; + this.sourceIp = sourceIp; + this.port = port; + this.intf = intf; + } + public PortConfig() { + + } + public boolean isAdd() { + return add; + } + public String getSourceIp() { + return sourceIp; + } + public String getPort() { + return port; + } + public String getIntf() { + return intf; + } + } + + static {//works since Request's static initializer is run before this one. + GsonBuilder s_gBuilder = Request.initBuilder(); + final Type listType = new TypeToken>() {}.getType(); + s_gBuilder.registerTypeAdapter(listType, new PortConfigListTypeAdaptor()); + } + + @Expose + private List portConfigs = new ArrayList(); + + public SecStorageFirewallCfgCommand() { + + } + + + public void addPortConfig(String sourceIp, String port, boolean add, String intf) { + PortConfig pc = new PortConfig(sourceIp, port, add, intf); + this.portConfigs.add(pc); + + } + + @Override + public boolean executeInSequence() { + return false; + } + + + public List getPortConfigs() { + return portConfigs; + } +} diff --git a/core/src/com/cloud/agent/api/SecStorageSetupCommand.java b/core/src/com/cloud/agent/api/SecStorageSetupCommand.java new file mode 100644 index 00000000000..1c15eb8087d --- /dev/null +++ b/core/src/com/cloud/agent/api/SecStorageSetupCommand.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class SecStorageSetupCommand extends Command { + private Long dcId; + String [] allowedInternalSites = new String[0]; + String copyUserName; + String copyPassword; + + public SecStorageSetupCommand() { + super(); + } + + public SecStorageSetupCommand(Long dcId) { + super(); + this.dcId = dcId; + } + + public Long getDataCenterId() { + return dcId; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String[] getAllowedInternalSites() { + return allowedInternalSites; + } + + public void setAllowedInternalSites(String[] allowedInternalSites) { + this.allowedInternalSites = allowedInternalSites; + } + + public String getCopyUserName() { + return copyUserName; + } + + public void setCopyUserName(String copyUserName) { + this.copyUserName = copyUserName; + } + + public String getCopyPassword() { + return copyPassword; + } + + public void setCopyPassword(String copyPassword) { + this.copyPassword = copyPassword; + } + +} diff --git a/core/src/com/cloud/agent/api/SetupAnswer.java b/core/src/com/cloud/agent/api/SetupAnswer.java new file mode 100644 index 00000000000..6475560bcf6 --- /dev/null +++ b/core/src/com/cloud/agent/api/SetupAnswer.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class SetupAnswer extends Answer { + public SetupAnswer() {} + + public SetupAnswer(SetupCommand cmd) { + super(cmd, true, null); + } + + public SetupAnswer(SetupCommand cmd, boolean result, String details) { + super(cmd, result, details); + } + + public SetupAnswer(SetupCommand cmd, String details) { + super(cmd, false, details); + } +} diff --git a/core/src/com/cloud/agent/api/SetupCommand.java b/core/src/com/cloud/agent/api/SetupCommand.java new file mode 100644 index 00000000000..5410d52e820 --- /dev/null +++ b/core/src/com/cloud/agent/api/SetupCommand.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.host.HostEnvironment; + +public class SetupCommand extends Command { + + HostEnvironment env; + boolean multipath; + + public SetupCommand(HostEnvironment env) { + this.env = env; + this.multipath = false; + } + + public HostEnvironment getEnvironment() { + return env; + } + + protected SetupCommand() { + } + + public void setMultipathOn() { + this.multipath = true; + } + + public boolean useMultipath() { + return multipath; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/com/cloud/agent/api/ShutdownCommand.java b/core/src/com/cloud/agent/api/ShutdownCommand.java new file mode 100644 index 00000000000..42144812b33 --- /dev/null +++ b/core/src/com/cloud/agent/api/ShutdownCommand.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +/** + * This command informs the server that the agent is shutting down. + * + */ +public class ShutdownCommand extends Command { + public static final String Requested = "sig.kill"; + public static final String Update = "update"; + public static final String Unknown = "unknown"; + + private String reason; + private String detail; + + protected ShutdownCommand() { + } + + public ShutdownCommand(String reason, String detail) { + this.reason = reason; + this.detail = detail; + } + + /** + * @return return the reason the agent shutdown. If Unknown, call getDetail() for any details. + */ + public String getReason() { + return reason; + } + + public String getDetail() { + return detail; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/SnapshotCommand.java b/core/src/com/cloud/agent/api/SnapshotCommand.java new file mode 100644 index 00000000000..72722b06778 --- /dev/null +++ b/core/src/com/cloud/agent/api/SnapshotCommand.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +/** + * When a snapshot of a VDI is taken, it creates two new files, + * a 'base copy' which contains all the new data since the time of the last snapshot and an 'empty snapshot' file. + * Any new data is again written to the VDI with the same UUID. + * This class issues a command for copying the 'base copy' vhd file to secondary storage. + * This currently assumes that both primary and secondary storage are mounted on the XenServer. + */ +public class SnapshotCommand extends Command { + private String primaryStoragePoolNameLabel; + private String snapshotUuid; + private String snapshotName; + private String secondaryStoragePoolURL; + private Long dcId; + private Long accountId; + private Long volumeId; + + protected SnapshotCommand() { + + } + + /** + * @param primaryStoragePoolNameLabel The primary storage Pool + * @param snapshotUuid The UUID of the snapshot which is going to be backed up + * @param secondaryStoragePoolURL This is what shows up in the UI when you click on Secondary storage. + * In the code, it is present as: In the vmops.host_details table, there is a field mount.parent. This is the value of that field + * If you have better ideas on how to get it, you are welcome. + */ + public SnapshotCommand(String primaryStoragePoolNameLabel, + String secondaryStoragePoolURL, + String snapshotUuid, + String snapshotName, + Long dcId, + Long accountId, + Long volumeId) + { + this.primaryStoragePoolNameLabel = primaryStoragePoolNameLabel; + this.snapshotUuid = snapshotUuid; + this.secondaryStoragePoolURL = secondaryStoragePoolURL; + this.dcId = dcId; + this.accountId = accountId; + this.volumeId = volumeId; + this.snapshotName = snapshotName; + } + + /** + * @return the primaryStoragePoolNameLabel + */ + public String getPrimaryStoragePoolNameLabel() { + return primaryStoragePoolNameLabel; + } + + /** + * @return the snapshotUuid + */ + public String getSnapshotUuid() { + return snapshotUuid; + } + + public String getSnapshotName() { + return snapshotName; + } + + /** + * @return the secondaryStoragePoolURL + */ + public String getSecondaryStoragePoolURL() { + return secondaryStoragePoolURL; + } + + + public Long getDataCenterId() { + return dcId; + } + + public Long getAccountId() { + return accountId; + } + + public Long getVolumeId() { + return volumeId; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean executeInSequence() { + return false; + } + +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/StartAnswer.java b/core/src/com/cloud/agent/api/StartAnswer.java new file mode 100755 index 00000000000..bec39b15237 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartAnswer.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +/** + * @author ahuang + * + */ +public class StartAnswer extends Answer { + + protected StartAnswer() { + } + + public StartAnswer(StartCommand cmd) { + super(cmd); + } + + public StartAnswer(StartCommand cmd, String details) { + super(cmd, false, details); + } +} diff --git a/core/src/com/cloud/agent/api/StartCommand.java b/core/src/com/cloud/agent/api/StartCommand.java new file mode 100755 index 00000000000..56747c8f6e6 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartCommand.java @@ -0,0 +1,198 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.List; +import java.util.Map; + +import com.cloud.offering.ServiceOffering; +import com.cloud.storage.VolumeVO; +import com.cloud.uservm.UserVm; +import com.cloud.vm.DomainRouter; +import com.cloud.vm.UserVmVO; + +public class StartCommand extends AbstractStartCommand { + long id; + String guestIpAddress; + String gateway; + int ramSize; + String imagePath; + String guestNetworkId; + String guestMacAddress; + String vncPassword; + String externalVlan; + String externalMacAddress; + int utilization; + int cpuWeight; + int cpu; + int networkRateMbps; + int networkRateMulticastMbps; + String hostName; + String arch; + String isoPath; + boolean bootFromISO; + String guestOSDescription; + + public StartCommand(UserVm vm, String vmName, ServiceOffering offering, int networkRate, int multicastRate, DomainRouter router, String storageHost, String imagePath, String guestNetworkId, int utilization, int cpuWeight, List vols, int bits, String isoPath, boolean bootFromISO, String guestOSDescription) { + super(vmName, storageHost, vols); + initialize(vm, offering, networkRate, multicastRate, router, imagePath, guestNetworkId, utilization, cpuWeight, bits, isoPath, bootFromISO, guestOSDescription); + } + + private void initialize(UserVm vm, + ServiceOffering offering, int networkRate, int multicastRate, DomainRouter router, String imagePath, + String guestNetworkId, int utilization, int cpuWeight, int bits, String isoPath, boolean bootFromISO, String guestOSDescription) { + id = vm.getId(); + guestIpAddress = vm.getGuestIpAddress(); + if (router != null) + gateway = router.getPrivateIpAddress(); + ramSize = offering.getRamSize(); + cpu = offering.getCpu(); + this.utilization= utilization; + this.cpuWeight = cpuWeight; + this.imagePath = imagePath; + this.guestNetworkId = guestNetworkId; + guestMacAddress = vm.getGuestMacAddress(); + vncPassword = vm.getVncPassword(); + hostName = vm.getName(); +// networkRateMbps = offering.getRateMbps(); +// networkRateMulticastMbps = offering.getMulticastRateMbps(); + networkRateMbps = networkRate; + networkRateMulticastMbps = multicastRate; + if (bits == 32) { + arch = "i686"; + } else { + arch = "x86_64"; + } + this.isoPath = isoPath; + this.bootFromISO = bootFromISO; + this.guestOSDescription = guestOSDescription; + } + + public String getArch() { + return arch; + } + + public String getHostName() { + return hostName; + } + + protected StartCommand() { + super(); + } + + public StartCommand(UserVmVO vm, String vmName, ServiceOffering offering, int networkRate, int multicastRate, + DomainRouter router, String[] storageIps, String imagePath, + String guestNetworkId, int utilization, int cpuWeight, List vols, + boolean mirroredVols, int bits, String isoPath, boolean bootFromISO, String guestOSDescription) { + super(vmName, storageIps, vols, mirroredVols); + initialize(vm, offering, networkRate, multicastRate, router, imagePath, guestNetworkId, utilization, cpuWeight, bits, isoPath, bootFromISO, guestOSDescription); + + } + + @Override + public boolean executeInSequence() { + return true; + } + + public int getCpu() { + return cpu; + } + + public int getUtilization() { + return utilization; + } + + public int getCpuWeight() { + return cpuWeight; + } + + public long getId() { + return id; + } + + public String getGuestIpAddress() { + return guestIpAddress; + } + + public String getGuestMacAddress() { + return guestMacAddress; + } + + public String getVncPassword() { + return vncPassword; + } + + public String getGateway() { + return gateway; + } + + public String getGuestNetworkId() { + return guestNetworkId; + } + + + public int getRamSize() { + return ramSize; + } + + public String getImagePath() { + return imagePath; + } + + public int getNetworkRateMbps() { + return networkRateMbps; + } + + public int getNetworkRateMulticastMbps() { + return networkRateMulticastMbps; + } + + public String getISOPath() { + return isoPath; + } + + public boolean getBootFromISO() { + return bootFromISO; + } + + public void setExternalVlan(String vlanId) { + this.externalVlan = vlanId; + + } + + public String getExternalVlan() { + return externalVlan; + } + + public String getExternalMacAddress() { + return externalMacAddress; + } + + public void setExternalMacAddress(String externalMacAddress) { + this.externalMacAddress = externalMacAddress; + } + + public String getGuestOSDescription() { + return this.guestOSDescription; + } + + public void setGuestOSDescription(String guestOSDescription) { + this.guestOSDescription = guestOSDescription; + } + +} diff --git a/core/src/com/cloud/agent/api/StartConsoleProxyAnswer.java b/core/src/com/cloud/agent/api/StartConsoleProxyAnswer.java new file mode 100644 index 00000000000..5c484633d28 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartConsoleProxyAnswer.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +public class StartConsoleProxyAnswer extends Answer { + String privateIpAddress; + String privateMacAddress; + + protected StartConsoleProxyAnswer() { + } + + public StartConsoleProxyAnswer(StartConsoleProxyCommand cmd) { + super(cmd); + } + + public StartConsoleProxyAnswer(StartConsoleProxyCommand cmd, String privateIp, String privateMac) { + super(cmd); + this.privateIpAddress = privateIp; + this.privateMacAddress = privateMac; + } + + public String getPrivateIpAddress() { + return privateIpAddress; + } + + public String getPrivateMacAddress() { + return privateMacAddress; + } + + public StartConsoleProxyAnswer(StartConsoleProxyCommand cmd, String details) { + super(cmd, false, details); + } +} diff --git a/core/src/com/cloud/agent/api/StartConsoleProxyCommand.java b/core/src/com/cloud/agent/api/StartConsoleProxyCommand.java new file mode 100644 index 00000000000..9b6faa4b893 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartConsoleProxyCommand.java @@ -0,0 +1,102 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api; + +import java.util.List; + +import com.cloud.storage.VolumeVO; +import com.cloud.vm.ConsoleProxyVO; + +public class StartConsoleProxyCommand extends AbstractStartCommand { + + private ConsoleProxyVO proxy; + private int proxyCmdPort; + private String vncPort; + private String urlPort; + private String mgmt_host; + private int mgmt_port; + private boolean sslEnabled; + + protected StartConsoleProxyCommand() { + } + + public StartConsoleProxyCommand(int proxyCmdPort, ConsoleProxyVO proxy, String vmName, String storageHost, + List vols, String vncPort, String urlPort, String mgmtHost, int mgmtPort, boolean sslEnabled) { + super(vmName, storageHost, vols); + this.proxyCmdPort = proxyCmdPort; + this.proxy = proxy; + this.vncPort = vncPort; + this.urlPort = urlPort; + this.mgmt_host = mgmtHost; + this.mgmt_port = mgmtPort; + this.sslEnabled = sslEnabled; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public ConsoleProxyVO getProxy() { + return proxy; + } + + public int getProxyCmdPort() { + return proxyCmdPort; + } + + public String getVNCPort() { + return this.vncPort; + } + + public String getURLPort() { + return this.urlPort; + } + + public String getManagementHost() { + return mgmt_host; + } + + public int getManagementPort() { + return mgmt_port; + } + + public String getBootArgs() { + String eth1Ip = (proxy.getPrivateIpAddress() == null)? "0.0.0.0":proxy.getPrivateIpAddress(); + String eth1NetMask = (proxy.getPrivateNetmask() == null) ? "0.0.0.0":proxy.getPrivateNetmask(); + String eth2Ip = (proxy.getPublicIpAddress() == null)?"0.0.0.0" : proxy.getPublicIpAddress(); + String eth2NetMask = (proxy.getPublicNetmask() == null) ? "0.0.0.0":proxy.getPublicNetmask(); + String gateWay = (proxy.getGateway() == null) ? "0.0.0.0" : proxy.getGateway(); + + String basic = " eth0ip=" + proxy.getGuestIpAddress() + " eth0mask=" + proxy.getGuestNetmask() + " eth1ip=" + + eth1Ip + " eth1mask=" + eth1NetMask + " eth2ip=" + + eth2Ip + " eth2mask=" + eth2NetMask + " gateway=" + gateWay + + " dns1=" + proxy.getDns1() + " type=consoleproxy"+ " name=" + proxy.getName() + " template=domP"; + if (proxy.getDns2() != null) { + basic = basic + " dns2=" + proxy.getDns2(); + } + basic = basic + " host=" + mgmt_host + " port=" + mgmt_port; + if(sslEnabled) + basic = basic + " premium=true"; + if (proxy.getPrivateIpAddress() == null || proxy.getPublicIpAddress() == null) { + basic = basic + " bootproto=dhcp"; + } + return basic; + } +} diff --git a/core/src/com/cloud/agent/api/StartRouterAnswer.java b/core/src/com/cloud/agent/api/StartRouterAnswer.java new file mode 100644 index 00000000000..b8b4f2ec603 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartRouterAnswer.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +/** + * @author chiradeep + * + */ +public class StartRouterAnswer extends Answer { + String privateIpAddress; + String privateMacAddress; + + protected StartRouterAnswer() { + } + + public StartRouterAnswer(StartRouterCommand cmd) { + super(cmd); + } + + public StartRouterAnswer(StartRouterCommand cmd, String privateIp, String privateMac) { + super(cmd); + this.privateIpAddress = privateIp; + this.privateMacAddress = privateMac; + } + + public String getPrivateIpAddress() { + return privateIpAddress; + } + + public String getPrivateMacAddress() { + return privateMacAddress; + } + + public StartRouterAnswer(StartRouterCommand cmd, String details) { + super(cmd, false, details); + } + +} diff --git a/core/src/com/cloud/agent/api/StartRouterCommand.java b/core/src/com/cloud/agent/api/StartRouterCommand.java new file mode 100755 index 00000000000..ec8ca7a808a --- /dev/null +++ b/core/src/com/cloud/agent/api/StartRouterCommand.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.List; + +import com.cloud.storage.VolumeVO; +import com.cloud.vm.DomainRouter; +import com.cloud.vm.DomainRouterVO; +import com.cloud.vm.DomainRouter.Role; + + +public class StartRouterCommand extends AbstractStartCommand { + + DomainRouterVO router; + + protected StartRouterCommand() { + super(); + } + + @Override + public boolean executeInSequence() { + return true; + } + + public StartRouterCommand(DomainRouterVO router, String routerName, String[] storageIps, List vols, boolean mirroredVols) { + super(routerName, storageIps, vols, mirroredVols); + this.router = router; + } + + public DomainRouter getRouter() { + return router; + } + + public String getBootArgs() { + String eth2Ip = router.getPublicIpAddress()==null?"0.0.0.0":router.getPublicIpAddress(); + String basic = " eth0ip=" + router.getGuestIpAddress() + " eth0mask=" + router.getGuestNetmask() + " eth1ip=" + + router.getPrivateIpAddress() + " eth1mask=" + router.getPrivateNetmask() + " gateway=" + router.getGateway() + + " dns1=" + router.getDns1() + " name=" + router.getName(); + if (!router.getPublicMacAddress().equalsIgnoreCase("FE:FF:FF:FF:FF:FF")) { + basic = basic + " eth2ip=" + eth2Ip + " eth2mask=" + router.getPublicNetmask(); + } + if (router.getDns2() != null) { + basic = basic + " dns2=" + router.getDns2(); + } + if (getDhcpRange() != null) { + basic = basic + " dhcprange=" + getDhcpRange(); + } + if (router.getRole() == Role.DHCP_FIREWALL_LB_PASSWD_USERDATA) { + basic = basic + " type=router"; + } else if (router.getRole() == Role.DHCP_USERDATA) { + basic = basic + " type=dhcpsrvr"; + } + return basic; + } + + public String getDhcpRange() { + String [] range = router.getDhcpRange(); + String result = null; + if (range[0] != null) { + result = range[0]; + } + return result; + } + + +} diff --git a/core/src/com/cloud/agent/api/StartSecStorageVmAnswer.java b/core/src/com/cloud/agent/api/StartSecStorageVmAnswer.java new file mode 100644 index 00000000000..6e2296a63b1 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartSecStorageVmAnswer.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class StartSecStorageVmAnswer extends Answer { + String privateIpAddress; + String privateMacAddress; + + protected StartSecStorageVmAnswer() { + } + + public StartSecStorageVmAnswer(StartSecStorageVmCommand cmd) { + super(cmd); + } + + public StartSecStorageVmAnswer(StartSecStorageVmCommand cmd, String privateIp, String privateMac) { + super(cmd); + this.privateIpAddress = privateIp; + this.privateMacAddress = privateMac; + } + + public String getPrivateIpAddress() { + return privateIpAddress; + } + + public String getPrivateMacAddress() { + return privateMacAddress; + } + + public StartSecStorageVmAnswer(StartSecStorageVmCommand cmd, String details) { + super(cmd, false, details); + } +} diff --git a/core/src/com/cloud/agent/api/StartSecStorageVmCommand.java b/core/src/com/cloud/agent/api/StartSecStorageVmCommand.java new file mode 100644 index 00000000000..c41070eb028 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartSecStorageVmCommand.java @@ -0,0 +1,102 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.net.URISyntaxException; +import java.util.List; + +import com.cloud.storage.VolumeVO; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NfsUtils; +import com.cloud.vm.SecondaryStorageVmVO; + +public class StartSecStorageVmCommand extends AbstractStartCommand { + + private SecondaryStorageVmVO secStorageVm; + private int proxyCmdPort; + private String mgmt_host; + private int mgmt_port; + private boolean sslCopy; + + protected StartSecStorageVmCommand() { + } + + public StartSecStorageVmCommand(int proxyCmdPort, SecondaryStorageVmVO secStorageVm, String vmName, String storageHost, + List vols, String mgmtHost, int mgmtPort, boolean sslCopy) { + super(vmName, storageHost, vols); + this.proxyCmdPort = proxyCmdPort; + this.secStorageVm = secStorageVm; + + this.mgmt_host = mgmtHost; + this.mgmt_port = mgmtPort; + this.sslCopy = sslCopy; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public SecondaryStorageVmVO getSecondaryStorageVmVO() { + return secStorageVm; + } + + public int getProxyCmdPort() { + return proxyCmdPort; + } + + + public String getManagementHost() { + return mgmt_host; + } + + public int getManagementPort() { + return mgmt_port; + } + + public String getBootArgs() { + String eth1Ip = (secStorageVm.getPrivateIpAddress() == null)? "0.0.0.0":secStorageVm.getPrivateIpAddress(); + String eth1NetMask = (secStorageVm.getPrivateNetmask() == null) ? "0.0.0.0":secStorageVm.getPrivateNetmask(); + String eth2Ip = (secStorageVm.getPublicIpAddress() == null)?"0.0.0.0" : secStorageVm.getPublicIpAddress(); + String eth2NetMask = (secStorageVm.getPublicNetmask() == null) ? "0.0.0.0":secStorageVm.getPublicNetmask(); + String gateWay = (secStorageVm.getGateway() == null) ? "0.0.0.0" : secStorageVm.getGateway(); + + String basic = " eth0ip=" + secStorageVm.getGuestIpAddress() + " eth0mask=" + secStorageVm.getGuestNetmask() + " eth1ip=" + + eth1Ip + " eth1mask=" + eth1NetMask + " eth2ip=" + + eth2Ip + " eth2mask=" + eth2NetMask + " gateway=" + gateWay + + " dns1=" + secStorageVm.getDns1() + " type=secstorage" + " name=" + secStorageVm.getName() + " template=domP"; + if (secStorageVm.getDns2() != null) { + basic = basic + " dns2=" + secStorageVm.getDns2(); + } + basic = basic + " host=" + mgmt_host + " port=" + mgmt_port; + String mountStr = null; + try { + mountStr = NfsUtils.url2Mount(secStorageVm.getNfsShare()); + } catch (URISyntaxException e1) { + throw new CloudRuntimeException("NFS url malformed in database? url=" + secStorageVm.getNfsShare()); + } + basic = basic + " mount.path=" + mountStr + " guid=" + secStorageVm.getGuid(); + basic = basic + " resource=com.cloud.storage.resource.NfsSecondaryStorageResource"; + basic = basic + " instance=SecStorage"; + basic = basic + " sslcopy=" + Boolean.toString(sslCopy); + if (secStorageVm.getPrivateIpAddress() == null || secStorageVm.getPublicIpAddress() == null) { + basic = basic + " bootproto=dhcp"; + } + return basic; + } +} diff --git a/core/src/com/cloud/agent/api/StartupAnswer.java b/core/src/com/cloud/agent/api/StartupAnswer.java new file mode 100755 index 00000000000..f30c0b05873 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartupAnswer.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +/** + * @author ahuang + * + */ +public class StartupAnswer extends Answer { + long hostId; + int pingInterval; + + protected StartupAnswer() { + } + + public StartupAnswer(StartupCommand cmd, long hostId, int pingInterval) { + super(cmd); + this.hostId = hostId; + this.pingInterval = pingInterval; + } + + public StartupAnswer(StartupCommand cmd, String details) { + super(cmd, false, details); + } + + public long getHostId() { + return hostId; + } + + public int getPingInterval() { + return pingInterval; + } +} diff --git a/core/src/com/cloud/agent/api/StartupCommand.java b/core/src/com/cloud/agent/api/StartupCommand.java new file mode 100755 index 00000000000..eeafda57a00 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartupCommand.java @@ -0,0 +1,261 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class StartupCommand extends Command { + String dataCenter; + String pod; + String cluster; + String guid; + String name; + Long id; + String version; + String iqn; + String publicIpAddress; + String publicNetmask; + String publicMacAddress; + String privateIpAddress; + String privateMacAddress; + String privateNetmask; + String storageIpAddress; + String storageNetmask; + String storageMacAddress; + String storageIpAddressDeux; + String storageMacAddressDeux; + String storageNetmaskDeux; + String agentTag; + String resourceName; + + public StartupCommand() { + } + + public StartupCommand(Long id, String name, String dataCenter, String pod, String guid, String version) { + super(); + this.id = id; + this.dataCenter = dataCenter; + this.pod = pod; + this.guid = guid; + this.name = name; + this.version = version; + } + + public String getIqn() { + return iqn; + } + + public void setCluster(String cluster) { + this.cluster = cluster; + } + + public String getCluster() { + return cluster; + } + + public void setIqn(String iqn) { + this.iqn = iqn; + } + + public String getDataCenter() { + return dataCenter; + } + + public String getPod() { + return pod; + } + + public Long getId() { + return id; + } + + public String getStorageIpAddressDeux() { + return storageIpAddressDeux; + } + + public void setStorageIpAddressDeux(String storageIpAddressDeux) { + this.storageIpAddressDeux = storageIpAddressDeux; + } + + public String getStorageMacAddressDeux() { + return storageMacAddressDeux; + } + + public void setStorageMacAddressDeux(String storageMacAddressDeux) { + this.storageMacAddressDeux = storageMacAddressDeux; + } + + public String getStorageNetmaskDeux() { + return storageNetmaskDeux; + } + + public void setStorageNetmaskDeux(String storageNetmaskDeux) { + this.storageNetmaskDeux = storageNetmaskDeux; + } + + public String getGuid() { + return guid; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public void setDataCenter(String dataCenter) { + this.dataCenter = dataCenter; + } + + public void setPod(String pod) { + this.pod = pod; + } + + public void setGuid(String guid) { + this.guid = guid; + } + + public void setGuid(String guid, String resourceName) { + this.resourceName = resourceName; + this.guid = guid + "-" + resourceName; + } + + public String getPublicNetmask() { + return publicNetmask; + } + + public void setPublicNetmask(String publicNetmask) { + this.publicNetmask = publicNetmask; + } + + public String getPublicMacAddress() { + return publicMacAddress; + } + + public void setPublicMacAddress(String publicMacAddress) { + this.publicMacAddress = publicMacAddress; + } + + public String getPrivateIpAddress() { + return privateIpAddress; + } + + public void setPrivateIpAddress(String privateIpAddress) { + this.privateIpAddress = privateIpAddress; + } + + public String getPrivateMacAddress() { + return privateMacAddress; + } + + public void setPrivateMacAddress(String privateMacAddress) { + this.privateMacAddress = privateMacAddress; + } + + public String getPrivateNetmask() { + return privateNetmask; + } + + public void setPrivateNetmask(String privateNetmask) { + this.privateNetmask = privateNetmask; + } + + public String getStorageIpAddress() { + return storageIpAddress; + } + + public void setStorageIpAddress(String storageIpAddress) { + this.storageIpAddress = storageIpAddress; + } + + public String getStorageNetmask() { + return storageNetmask; + } + + public void setStorageNetmask(String storageNetmask) { + this.storageNetmask = storageNetmask; + } + + public String getStorageMacAddress() { + return storageMacAddress; + } + + public void setStorageMacAddress(String storageMacAddress) { + this.storageMacAddress = storageMacAddress; + } + + public String getPublicIpAddress() { + return publicIpAddress; + } + + public void setName(String name) { + this.name = name; + } + + public void setId(Long id) { + this.id = id; + } + + public void setVersion(String version) { + this.version = version; + } + + public void setPublicIpAddress(String publicIpAddress) { + this.publicIpAddress = publicIpAddress; + } + + public String getAgentTag() { + return agentTag; + } + + public void setAgentTag(String tag) { + agentTag = tag; + } + + public void setResourceName(String resourceName) { + this.resourceName = resourceName; + } + + public String getGuidWithoutResource() { + if (resourceName == null) + return guid; + else { + int hyph = guid.lastIndexOf('-'); + if (hyph == -1) { + return guid; + } + String tmpResource = guid.substring(hyph+1, guid.length()); + if (resourceName.equals(tmpResource)){ + return guid.substring(0, hyph); + } else { + return guid; + } + } + } + + public String getResourceName() { + return resourceName; + } + + + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/StartupProxyCommand.java b/core/src/com/cloud/agent/api/StartupProxyCommand.java new file mode 100644 index 00000000000..a57dab95817 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartupProxyCommand.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class StartupProxyCommand extends StartupCommand { + private int proxyPort; + private long proxyVmId; + + public StartupProxyCommand() { + super(); + setIqn("NoIqn"); + } + + @Override + public boolean executeInSequence() { + return true; + } + + public int getProxyPort() { + return proxyPort; + } + + public void setProxyPort(int proxyPort) { + this.proxyPort = proxyPort; + } + + public long getProxyVmId() { + return proxyVmId; + } + public void setProxyVmId(long proxyVmId) { + this.proxyVmId = proxyVmId; + } +} diff --git a/core/src/com/cloud/agent/api/StartupRoutingCommand.java b/core/src/com/cloud/agent/api/StartupRoutingCommand.java new file mode 100755 index 00000000000..deb2a5aba24 --- /dev/null +++ b/core/src/com/cloud/agent/api/StartupRoutingCommand.java @@ -0,0 +1,155 @@ +/** Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.HashMap; +import java.util.Map; + +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.Hypervisor.Type; +import com.cloud.network.NetworkEnums.RouterPrivateIpStrategy; +import com.cloud.vm.State; + +public class StartupRoutingCommand extends StartupCommand { + int cpus; + long speed; + long memory; + long dom0MinMemory; + Map vms; + String caps; + String pool; + Hypervisor.Type hypervisorType; + Map hostDetails; //stuff like host os, cpu capabilities + + public StartupRoutingCommand() { + super(); + hostDetails = new HashMap(); + getHostDetails().put(RouterPrivateIpStrategy.class.getCanonicalName(), RouterPrivateIpStrategy.DcGlobal.toString()); + + } + + public StartupRoutingCommand(int cpus, + long speed, + long memory, + long dom0MinMemory, + String caps, + Hypervisor.Type hypervisorType, + RouterPrivateIpStrategy privIpStrategy, + Map vms) { + this(cpus, speed, memory, dom0MinMemory, caps, hypervisorType, vms); + getHostDetails().put(RouterPrivateIpStrategy.class.getCanonicalName(), privIpStrategy.toString()); + } + + public StartupRoutingCommand(int cpus, + long speed, + long memory, + long dom0MinMemory, + final String caps, + final Hypervisor.Type hypervisorType, + + final Map hostDetails, + Map vms) { + super(); + this.cpus = cpus; + this.speed = speed; + this.memory = memory; + this.dom0MinMemory = dom0MinMemory; + this.vms = vms; + this.hypervisorType = hypervisorType; + this.hostDetails = hostDetails; + this.caps = caps; + } + + public StartupRoutingCommand(int cpus2, long speed2, long memory2, + long dom0MinMemory2, String caps2, Hypervisor.Type hypervisorType2, + Map vms2) { + this(cpus2, speed2, memory2, dom0MinMemory2, caps2, hypervisorType2, new HashMap(), vms2); + } + + public void setChanges(Map vms) { + this.vms = vms; + } + + public int getCpus() { + return cpus; + } + + public String getCapabilities() { + return caps; + } + + public long getSpeed() { + return speed; + } + + public long getMemory() { + return memory; + } + + public long getDom0MinMemory() { + return dom0MinMemory; + } + + public Map getVmStates() { + return vms; + } + + public void setSpeed(long speed) { + this.speed = speed; + } + + public void setCpus(int cpus) { + this.cpus = cpus; + } + + public void setMemory(long memory) { + this.memory = memory; + } + + public void setDom0MinMemory(long dom0MinMemory) { + this.dom0MinMemory = dom0MinMemory; + } + + public void setCaps(String caps) { + this.caps = caps; + } + + public String getPool() { + return pool; + } + + public void setPool(String pool) { + this.pool = pool; + } + + public Hypervisor.Type getHypervisorType() { + return hypervisorType; + } + + public void setHypervisorType(Hypervisor.Type hypervisorType) { + this.hypervisorType = hypervisorType; + } + + public Map getHostDetails() { + return hostDetails; + } + + public void setHostDetails(Map hostDetails) { + this.hostDetails = hostDetails; + } +} + diff --git a/core/src/com/cloud/agent/api/StartupStorageCommand.java b/core/src/com/cloud/agent/api/StartupStorageCommand.java new file mode 100755 index 00000000000..a698f58468d --- /dev/null +++ b/core/src/com/cloud/agent/api/StartupStorageCommand.java @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.HashMap; +import java.util.Map; + +import com.cloud.storage.Volume; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.template.TemplateInfo; + + +public class StartupStorageCommand extends StartupCommand { + + String parent; + Map templateInfo; + long totalSize; + StoragePoolInfo poolInfo; + Volume.StorageResourceType resourceType; + StoragePoolType fsType; + Map hostDetails = new HashMap(); + String nfsShare; + + public StartupStorageCommand() { + super(); + } + + public StartupStorageCommand(String parent, StoragePoolType fsType, long totalSize, Map info) { + super(); + this.parent = parent; + this.totalSize = totalSize; + this.templateInfo = info; + this.poolInfo = null; + this.fsType = fsType; + } + + + public StartupStorageCommand(String parent, StoragePoolType fsType, Map templateInfo, StoragePoolInfo poolInfo) { + super(); + this.parent = parent; + this.templateInfo = templateInfo; + this.totalSize = poolInfo.capacityBytes; + this.poolInfo = poolInfo; + this.fsType = fsType; + } + + public String getParent() { + return parent; + } + + public void setNfsShare(String nfsShare) { + this.nfsShare = nfsShare; + } + + public String getNfsShare() { + return nfsShare; + } + + public long getTotalSize() { + return totalSize; + } + + public Map getTemplateInfo() { + return templateInfo; + } + + public void setTemplateInfo(Map templateInfo) { + this.templateInfo = templateInfo; + } + + public StoragePoolInfo getPoolInfo() { + return poolInfo; + } + + public void setPoolInfo(StoragePoolInfo poolInfo) { + this.poolInfo = poolInfo; + } + + public Volume.StorageResourceType getResourceType() { + return resourceType; + } + + public void setResourceType(Volume.StorageResourceType resourceType) { + this.resourceType = resourceType; + } + + /*For secondary storage*/ + public Map getHostDetails() { + return hostDetails; + } +} diff --git a/core/src/com/cloud/agent/api/StopAnswer.java b/core/src/com/cloud/agent/api/StopAnswer.java new file mode 100755 index 00000000000..fef9d405e19 --- /dev/null +++ b/core/src/com/cloud/agent/api/StopAnswer.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class StopAnswer extends RebootAnswer { + Integer vncPort; + Long bytesSent; + Long bytesReceived; + + protected StopAnswer() { + } + + public StopAnswer(StopCommand cmd, String details, Integer vncPort, Long bytesSent, Long bytesReceived) { + super(cmd, details, bytesSent, bytesReceived); + this.vncPort = vncPort; + } + + public StopAnswer(StopCommand cmd, String details) { + super(cmd, details); + vncPort = null; + + } + + public Integer getVncPort() { + return vncPort; + } + + public Long getBytesReceived() { + return bytesReceived; + } + + public Long getBytesSent() { + return bytesSent; + } +} diff --git a/core/src/com/cloud/agent/api/StopCommand.java b/core/src/com/cloud/agent/api/StopCommand.java new file mode 100755 index 00000000000..f4e3d24d092 --- /dev/null +++ b/core/src/com/cloud/agent/api/StopCommand.java @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.vm.VirtualMachine; + +public class StopCommand extends RebootCommand { + String vnet; + boolean mirroredVolumes=false; + private boolean isProxy=false; + private String vncPort=null; + private String urlPort=null; + private String publicConsoleProxyIpAddress=null; + private String privateRouterIpAddress=null; + + protected StopCommand() { + } + + public StopCommand(VirtualMachine vm, boolean isProxy, String vncPort, String urlPort, String publicConsoleProxyIpAddress) { + super(vm); + this.isProxy = isProxy; + this.vncPort = vncPort; + this.urlPort = urlPort; + this.publicConsoleProxyIpAddress = publicConsoleProxyIpAddress; + } + + public StopCommand(VirtualMachine vm, String vnet) { + super(vm); + this.vnet = vnet; + } + + public StopCommand(VirtualMachine vm, String vmName, String vnet) { + super(vmName); + this.vnet = vnet; + } + + public StopCommand(VirtualMachine vm, String vmName, String vnet, String privateRouterIpAddress) { + super(vmName); + this.vnet = vnet; + this.privateRouterIpAddress = privateRouterIpAddress; + } + + public String getVnet() { + return vnet; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public boolean isMirroredVolumes() { + return mirroredVolumes; + } + + public void setMirroredVolumes(boolean mirroredVolumes) { + this.mirroredVolumes = mirroredVolumes; + } + + public boolean isProxy() { + return this.isProxy; + } + + public String getVNCPort() { + return this.vncPort; + } + + public String getURLPort() { + return this.urlPort; + } + + public String getPublicConsoleProxyIpAddress() { + return this.publicConsoleProxyIpAddress; + } + + public String getPrivateRouterIpAddress() { + return privateRouterIpAddress; + } + +} diff --git a/core/src/com/cloud/agent/api/StoragePoolInfo.java b/core/src/com/cloud/agent/api/StoragePoolInfo.java new file mode 100644 index 00000000000..6b5108b8029 --- /dev/null +++ b/core/src/com/cloud/agent/api/StoragePoolInfo.java @@ -0,0 +1,98 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import java.util.Map; + +import com.cloud.storage.Storage.StoragePoolType; + +public class StoragePoolInfo { + String name; + String uuid; + String host; + String localPath; + String hostPath; + StoragePoolType poolType; + long capacityBytes; + long availableBytes; + Map details; + + protected StoragePoolInfo() { + super(); + } + + public StoragePoolInfo(String name, String uuid, String host, String hostPath, + String localPath, StoragePoolType poolType, long capacityBytes, + long availableBytes) { + super(); + this.name = name; + this.uuid = uuid; + this.host = host; + this.localPath = localPath; + this.hostPath = hostPath; + this.poolType = poolType; + this.capacityBytes = capacityBytes; + this.availableBytes = availableBytes; + } + + public StoragePoolInfo(String name, String uuid, String host, String hostPath, + String localPath, StoragePoolType poolType, long capacityBytes, + long availableBytes, Map details) { + this(name, uuid, host, hostPath, localPath, poolType, capacityBytes, availableBytes); + this.details = details; + } + + public long getCapacityBytes() { + return capacityBytes; + } + public long getAvailableBytes() { + return availableBytes; + } + + public String getUuid() { + return uuid; + } + public String getHost() { + return host; + } + public String getLocalPath() { + return localPath; + } + public String getHostPath() { + return hostPath; + } + public StoragePoolType getPoolType() { + return poolType; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getDetails() { + return details; + } +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/UnsupportedAnswer.java b/core/src/com/cloud/agent/api/UnsupportedAnswer.java new file mode 100644 index 00000000000..464031168b7 --- /dev/null +++ b/core/src/com/cloud/agent/api/UnsupportedAnswer.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class UnsupportedAnswer extends Answer { + protected UnsupportedAnswer() { + super(); + } + + public UnsupportedAnswer(Command cmd, String details) { + super(cmd, false, details); + } +} diff --git a/core/src/com/cloud/agent/api/UpgradeAnswer.java b/core/src/com/cloud/agent/api/UpgradeAnswer.java new file mode 100644 index 00000000000..222a2bafa3b --- /dev/null +++ b/core/src/com/cloud/agent/api/UpgradeAnswer.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class UpgradeAnswer extends Answer { + protected UpgradeAnswer() { + } + + public UpgradeAnswer(UpgradeCommand cmd, String failure) { + super(cmd, false, failure); + } +} diff --git a/core/src/com/cloud/agent/api/UpgradeCommand.java b/core/src/com/cloud/agent/api/UpgradeCommand.java new file mode 100644 index 00000000000..b6ae928b136 --- /dev/null +++ b/core/src/com/cloud/agent/api/UpgradeCommand.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +/** + * UpgradeCommand is sent when the agent should update. + */ +public class UpgradeCommand extends Command { + + String url; + + protected UpgradeCommand() { + } + + public UpgradeCommand(String url) { + this.url = url; + } + + public String getUpgradeUrl() { + return url; + } + + @Override + public boolean executeInSequence() { + return true; + } + +} diff --git a/core/src/com/cloud/agent/api/ValidateSnapshotAnswer.java b/core/src/com/cloud/agent/api/ValidateSnapshotAnswer.java new file mode 100644 index 00000000000..7b1711120b4 --- /dev/null +++ b/core/src/com/cloud/agent/api/ValidateSnapshotAnswer.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + + +public class ValidateSnapshotAnswer extends Answer { + private String expectedSnapshotBackupUuid; + private String actualSnapshotBackupUuid; + private String actualSnapshotUuid; + + protected ValidateSnapshotAnswer() { + + } + + public ValidateSnapshotAnswer(ValidateSnapshotCommand cmd, boolean success, String result, String expectedSnapshotBackupUuid, String actualSnapshotBackupUuid, String actualSnapshotUuid) { + super(cmd, success, result); + this.expectedSnapshotBackupUuid = expectedSnapshotBackupUuid; + this.actualSnapshotBackupUuid = actualSnapshotBackupUuid; + this.actualSnapshotUuid = actualSnapshotUuid; + } + + /** + * @return the expectedSnapshotBackupUuid + */ + public String getExpectedSnapshotBackupUuid() { + return expectedSnapshotBackupUuid; + } + + /** + * @return the actualSnapshotBackupUuid + */ + public String getActualSnapshotBackupUuid() { + return actualSnapshotBackupUuid; + } + + public String getActualSnapshotUuid() { + return actualSnapshotUuid; + } +} diff --git a/core/src/com/cloud/agent/api/ValidateSnapshotCommand.java b/core/src/com/cloud/agent/api/ValidateSnapshotCommand.java new file mode 100644 index 00000000000..36862a938b3 --- /dev/null +++ b/core/src/com/cloud/agent/api/ValidateSnapshotCommand.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +public class ValidateSnapshotCommand extends Command { + private String primaryStoragePoolNameLabel; + private String volumeUuid; + private String firstBackupUuid; + private String previousSnapshotUuid; + private String templateUuid; + + protected ValidateSnapshotCommand() { + + } + + /** + * @param primaryStoragePoolNameLabel The primary storage Pool Name Label + * @param volumeUuid The UUID of the volume for which the snapshot was taken + * @param firstBackupUuid This UUID of the first snapshot that was ever taken for this volume, even it was deleted. + * @param previousSnapshotUuid The UUID of the previous snapshot on the primary. + * @param templateUuid If this is a root volume and no snapshot has been taken for it, + * this is the UUID of the template VDI. + */ + public ValidateSnapshotCommand(String primaryStoragePoolNameLabel, + String volumeUuid, + String firstBackupUuid, + String previousSnapshotUuid, + String templateUuid) + { + this.primaryStoragePoolNameLabel = primaryStoragePoolNameLabel; + this.volumeUuid = volumeUuid; + this.firstBackupUuid = firstBackupUuid; + this.previousSnapshotUuid = previousSnapshotUuid; + this.templateUuid = templateUuid; + } + + public String getPrimaryStoragePoolNameLabel() { + return primaryStoragePoolNameLabel; + } + + /** + * @return the volumeUuid + */ + public String getVolumeUuid() { + return volumeUuid; + } + + /** + * @return the firstBackupUuid + */ + public String getFirstBackupUuid() { + return firstBackupUuid; + } + + public String getPreviousSnapshotUuid() { + return previousSnapshotUuid; + } + + /** + * @return the templateUuid + */ + public String getTemplateUuid() { + return templateUuid; + } + + @Override + public boolean executeInSequence() { + return false; + } +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/VmStatsEntry.java b/core/src/com/cloud/agent/api/VmStatsEntry.java new file mode 100755 index 00000000000..dce24f2db70 --- /dev/null +++ b/core/src/com/cloud/agent/api/VmStatsEntry.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api; + +import com.cloud.vm.VmStats; + +/** + * @author ahuang + * + */ +public class VmStatsEntry implements VmStats { + + double cpuUtilization; + double networkReadKBs; + double networkWriteKBs; + int numCPUs; + String entityType; + + public VmStatsEntry() { + } + + public VmStatsEntry(double cpuUtilization, double networkReadKBs, double networkWriteKBs, int numCPUs, String entityType) + { + this.cpuUtilization = cpuUtilization; + this.networkReadKBs = networkReadKBs; + this.networkWriteKBs = networkWriteKBs; + this.numCPUs = numCPUs; + this.entityType = entityType; + } + + public double getCPUUtilization() { + return cpuUtilization; + } + + public void setCPUUtilization(double cpuUtilization) { + this.cpuUtilization = cpuUtilization; + } + + public double getNetworkReadKBs() { + return networkReadKBs; + } + + public void setNetworkReadKBs(double networkReadKBs) { + this.networkReadKBs = networkReadKBs; + } + + public double getNetworkWriteKBs() { + return networkWriteKBs; + } + + public void setNetworkWriteKBs(double networkWriteKBs) { + this.networkWriteKBs = networkWriteKBs; + } + + public int getNumCPUs() { + return numCPUs; + } + + public void setNumCPUs(int numCPUs) { + this.numCPUs = numCPUs; + } + + public String getEntityType(){ + return this.entityType; + } + + public void setEntityType(String entityType){ + this.entityType = entityType; + } + + +} diff --git a/core/src/com/cloud/agent/api/proxy/CheckConsoleProxyLoadCommand.java b/core/src/com/cloud/agent/api/proxy/CheckConsoleProxyLoadCommand.java new file mode 100644 index 00000000000..70eef5f3f4b --- /dev/null +++ b/core/src/com/cloud/agent/api/proxy/CheckConsoleProxyLoadCommand.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.proxy; + + +/** + * CheckConsoleProxyLoadCommand implements one-shot console proxy load-scan command + */ +public class CheckConsoleProxyLoadCommand extends ProxyCommand { + + private long proxyVmId; + private String proxyVmName; + private String proxyManagementIp; + private int proxyCmdPort; + + public CheckConsoleProxyLoadCommand() { + } + + public CheckConsoleProxyLoadCommand(long proxyVmId, String proxyVmName, String proxyManagementIp, int proxyCmdPort) { + this.proxyVmId = proxyVmId; + this.proxyVmName = proxyVmName; + this.proxyManagementIp = proxyManagementIp; + this.proxyCmdPort = proxyCmdPort; + } + + public long getProxyVmId() { + return proxyVmId; + } + + public String getProxyVmName() { + return proxyVmName; + } + + public String getProxyManagementIp() { + return proxyManagementIp; + } + + public int getProxyCmdPort() { + return proxyCmdPort; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/proxy/ConsoleProxyLoadAnswer.java b/core/src/com/cloud/agent/api/proxy/ConsoleProxyLoadAnswer.java new file mode 100644 index 00000000000..fb67b09e9ba --- /dev/null +++ b/core/src/com/cloud/agent/api/proxy/ConsoleProxyLoadAnswer.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.proxy; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +public class ConsoleProxyLoadAnswer extends Answer { + + private long proxyVmId; + private String proxyVmName; + + protected ConsoleProxyLoadAnswer() { + } + + public ConsoleProxyLoadAnswer(Command command, long proxyVmId, String proxyVmName, boolean success, String details) { + super(command, success, details); + + this.proxyVmId = proxyVmId; + this.proxyVmName = proxyVmName; + } + + public long getProxyVmId() { + return proxyVmId; + } + + public String getProxyVmName() { + return proxyVmName; + } +} diff --git a/core/src/com/cloud/agent/api/proxy/ProxyCommand.java b/core/src/com/cloud/agent/api/proxy/ProxyCommand.java new file mode 100644 index 00000000000..1bf395b6afa --- /dev/null +++ b/core/src/com/cloud/agent/api/proxy/ProxyCommand.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.proxy; + +import com.cloud.agent.api.Command; + +public abstract class ProxyCommand extends Command { + protected ProxyCommand() { + super(); + } +} diff --git a/core/src/com/cloud/agent/api/proxy/WatchConsoleProxyLoadCommand.java b/core/src/com/cloud/agent/api/proxy/WatchConsoleProxyLoadCommand.java new file mode 100644 index 00000000000..7d14d2e5f85 --- /dev/null +++ b/core/src/com/cloud/agent/api/proxy/WatchConsoleProxyLoadCommand.java @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.proxy; + +import com.cloud.agent.api.CronCommand; + +public class WatchConsoleProxyLoadCommand extends ProxyCommand implements CronCommand { + + private long proxyVmId; + private String proxyVmName; + private String proxyManagementIp; + private int proxyCmdPort; + int interval; + + public WatchConsoleProxyLoadCommand(int interval, long proxyVmId, String proxyVmName, + String proxyManagementIp, int proxyCmdPort) { + this.interval = interval; + this.proxyVmId = proxyVmId; + this.proxyVmName = proxyVmName; + this.proxyManagementIp = proxyManagementIp; + this.proxyCmdPort = proxyCmdPort; + } + + protected WatchConsoleProxyLoadCommand() { + } + + public long getProxyVmId() { + return proxyVmId; + } + + public String getProxyVmName() { + return proxyVmName; + } + + public String getProxyManagementIp() { + return proxyManagementIp; + } + + public int getProxyCmdPort() { + return proxyCmdPort; + } + + public int getInterval() { + return interval; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/com/cloud/agent/api/routing/DhcpEntryCommand.java b/core/src/com/cloud/agent/api/routing/DhcpEntryCommand.java new file mode 100644 index 00000000000..e49035c85a6 --- /dev/null +++ b/core/src/com/cloud/agent/api/routing/DhcpEntryCommand.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.routing; + + +public class DhcpEntryCommand extends RoutingCommand { + + String vmMac; + String vmIpAddress; + String routerPrivateIpAddress; + String vmName; + + protected DhcpEntryCommand() { + + } + + @Override + public boolean executeInSequence() { + return true; + } + + public DhcpEntryCommand(String vmMac, String vmIpAddress, String routerPrivateIpAddress, String vmName) { + this.vmMac = vmMac; + this.vmIpAddress = vmIpAddress; + this.routerPrivateIpAddress = routerPrivateIpAddress; + this.vmName = vmName; + } + + public String getVmMac() { + return vmMac; + } + + public String getRouterPrivateIpAddress() { + return routerPrivateIpAddress; + } + + public String getVmIpAddress() { + return vmIpAddress; + } + + public String getVmName() { + return vmName; + } + +} diff --git a/core/src/com/cloud/agent/api/routing/IPAssocCommand.java b/core/src/com/cloud/agent/api/routing/IPAssocCommand.java new file mode 100644 index 00000000000..ced18f26737 --- /dev/null +++ b/core/src/com/cloud/agent/api/routing/IPAssocCommand.java @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.routing; + + +/** + * @author chiradeep + * + */ +public class IPAssocCommand extends RoutingCommand { + + private String routerName; + private String routerIp; + private String publicIp; + private boolean sourceNat; + private boolean add; + private boolean firstIP; + private String vlanId; + private String vlanGateway; + private String vlanNetmask; + private String vifMacAddress; + + protected IPAssocCommand() { + } + + public IPAssocCommand(String routerName, String privateIpAddress, String ipAddress, boolean add, boolean firstIP, boolean sourceNat, String vlanId, String vlanGateway, String vlanNetmask, String vifMacAddress) { + this.setRouterName(routerName); + this.routerIp = privateIpAddress; + this.publicIp = ipAddress; + this.add = add; + this.firstIP = firstIP; + this.sourceNat = sourceNat; + this.vlanId = vlanId; + this.vlanGateway = vlanGateway; + this.vlanNetmask = vlanNetmask; + this.vifMacAddress = vifMacAddress; + } + + public String getRouterIp() { + return routerIp; + } + + public String getPublicIp() { + return publicIp; + } + + public boolean isAdd() { + return add; + } + + public boolean isFirstIP() { + return firstIP; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public void setRouterName(String routerName) { + this.routerName = routerName; + } + + public String getRouterName() { + return routerName; + } + + public void setSourceNat(boolean sourceNat) { + this.sourceNat = sourceNat; + } + + public boolean isSourceNat() { + return sourceNat; + } + + public String getVlanId() { + return vlanId; + } + + public String getVlanGateway() { + return vlanGateway; + } + + public String getVlanNetmask() { + return vlanNetmask; + } + + public String getVifMacAddress() { + return vifMacAddress; + } +} diff --git a/core/src/com/cloud/agent/api/routing/LoadBalancerCfgCommand.java b/core/src/com/cloud/agent/api/routing/LoadBalancerCfgCommand.java new file mode 100644 index 00000000000..fd2c38e98ba --- /dev/null +++ b/core/src/com/cloud/agent/api/routing/LoadBalancerCfgCommand.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.routing; + +import com.cloud.network.LoadBalancerConfigurator; + +/** + * @author chiradeep + * + */ +public class LoadBalancerCfgCommand extends RoutingCommand { + private String [] config; + private String [] addFwRules; + private String [] removeFwRules;; + private String routerName; + private String routerIp; + + //no-args to satisfy gson + protected LoadBalancerCfgCommand() { + + } + + public LoadBalancerCfgCommand(String[] config, String[][] addRemoveRules, String routerName, String routerIp) { + super(); + this.config = config; + this.addFwRules = addRemoveRules[LoadBalancerConfigurator.ADD]; + this.removeFwRules = addRemoveRules[LoadBalancerConfigurator.REMOVE]; + this.routerName = routerName; + this.routerIp = routerIp; + } + + public String getRouterName() { + return routerName; + } + + public String getRouterIp() { + return routerIp; + } + + public String[] getConfig() { + return config; + } + + public void setConfig(String[] config) { + this.config = config; + } + + public String[] getAddFwRules() { + return addFwRules; + } + + public String[] getRemoveFwRules() { + return removeFwRules; + } + + @Override + public boolean executeInSequence() { + return false; + } + +} diff --git a/core/src/com/cloud/agent/api/routing/RoutingCommand.java b/core/src/com/cloud/agent/api/routing/RoutingCommand.java new file mode 100644 index 00000000000..36cc89e58dc --- /dev/null +++ b/core/src/com/cloud/agent/api/routing/RoutingCommand.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.routing; + +import com.cloud.agent.api.Command; + +public abstract class RoutingCommand extends Command { + protected RoutingCommand() { + super(); + } +} diff --git a/core/src/com/cloud/agent/api/routing/SavePasswordCommand.java b/core/src/com/cloud/agent/api/routing/SavePasswordCommand.java new file mode 100644 index 00000000000..fa11ef1af96 --- /dev/null +++ b/core/src/com/cloud/agent/api/routing/SavePasswordCommand.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.routing; + + +public class SavePasswordCommand extends RoutingCommand { + + String password; + String vmIpAddress; + String routerPrivateIpAddress; + String vmName; + + protected SavePasswordCommand() { + + } + + @Override + public boolean executeInSequence() { + return true; + } + + public SavePasswordCommand(String password, String vmIpAddress, String routerPrivateIpAddress, String vmName) { + this.password = password; + this.vmIpAddress = vmIpAddress; + this.routerPrivateIpAddress = routerPrivateIpAddress; + this.vmName = vmName; + } + + public String getPassword() { + return password; + } + + public String getRouterPrivateIpAddress() { + return routerPrivateIpAddress; + } + + public String getVmIpAddress() { + return vmIpAddress; + } + + public String getVmName() { + return vmName; + } + +} diff --git a/core/src/com/cloud/agent/api/routing/SetFirewallRuleCommand.java b/core/src/com/cloud/agent/api/routing/SetFirewallRuleCommand.java new file mode 100755 index 00000000000..c46d78d8138 --- /dev/null +++ b/core/src/com/cloud/agent/api/routing/SetFirewallRuleCommand.java @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.routing; + +import com.cloud.network.FirewallRuleVO; + +public class SetFirewallRuleCommand extends RoutingCommand { + FirewallRuleVO rule; + String routerName; + String routerIpAddress; + String oldPrivateIP = null; + String oldPrivatePort = null; + + protected SetFirewallRuleCommand() { + } + + public SetFirewallRuleCommand(String routerName, String routerIpAddress, FirewallRuleVO rule, String oldPrivateIP, String oldPrivatePort) { + this.routerName = routerName; + this.routerIpAddress = routerIpAddress; + this.rule = rule; + this.oldPrivateIP = oldPrivateIP; + this.oldPrivatePort = oldPrivatePort; + } + + public SetFirewallRuleCommand(String routerName, String routerIpAddress, FirewallRuleVO rule) { + this.routerName = routerName; + this.routerIpAddress = routerIpAddress; + this.rule = rule; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public FirewallRuleVO getRule() { + return rule; + } + + public String getPrivateIpAddress() { + return rule.getPrivateIpAddress(); + } + + public String getPublicIpAddress() { + return rule.getPublicIpAddress(); + } + + public String getVlanNetmask() { + return rule.getVlanNetmask(); + } + + public String getPublicPort() { + return rule.getPublicPort(); + } + + public String getPrivatePort() { + return rule.getPrivatePort(); + } + + public String getRouterName() { + return routerName; + } + + public String getRouterIpAddress() { + return routerIpAddress; + } + + public boolean isEnable() { + return rule.isEnabled(); + } + + public String getProtocol() { + return rule.getProtocol(); + } + + public String getOldPrivateIP() { + return this.oldPrivateIP; + } + + public String getOldPrivatePort() { + return this.oldPrivatePort; + } + +} diff --git a/core/src/com/cloud/agent/api/routing/UserDataCommand.java b/core/src/com/cloud/agent/api/routing/UserDataCommand.java new file mode 100644 index 00000000000..184fe5251b8 --- /dev/null +++ b/core/src/com/cloud/agent/api/routing/UserDataCommand.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.routing; + + +public class UserDataCommand extends RoutingCommand { + + String userData; + String vmIpAddress; + String routerPrivateIpAddress; + String vmName; + + protected UserDataCommand() { + + } + + @Override + public boolean executeInSequence() { + return true; + } + + public UserDataCommand(String userData, String vmIpAddress, String routerPrivateIpAddress, String vmName) { + this.userData = userData; + this.vmIpAddress = vmIpAddress; + this.routerPrivateIpAddress = routerPrivateIpAddress; + this.vmName = vmName; + } + + public String getRouterPrivateIpAddress() { + return routerPrivateIpAddress; + } + + public String getVmIpAddress() { + return vmIpAddress; + } + + public String getVmName() { + return vmName; + } + + public String getUserData() { + return userData; + } + + public void setUserData(String userData) { + this.userData = userData; + } + +} diff --git a/core/src/com/cloud/agent/api/routing/VmDataCommand.java b/core/src/com/cloud/agent/api/routing/VmDataCommand.java new file mode 100644 index 00000000000..b7a5277e780 --- /dev/null +++ b/core/src/com/cloud/agent/api/routing/VmDataCommand.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.routing; + +import java.util.ArrayList; +import java.util.List; + +public class VmDataCommand extends RoutingCommand { + + String routerPrivateIpAddress; + String vmIpAddress; + List vmData; + + protected VmDataCommand() { + } + + @Override + public boolean executeInSequence() { + return true; + } + + public VmDataCommand(String routerPrivateIpAddress, String vmIpAddress) { + this.routerPrivateIpAddress = routerPrivateIpAddress; + this.vmIpAddress = vmIpAddress; + this.vmData = new ArrayList(); + } + + public String getRouterPrivateIpAddress() { + return routerPrivateIpAddress; + } + + public String getVmIpAddress() { + return vmIpAddress; + } + + public List getVmData() { + return vmData; + } + + public void addVmData(String folder, String file, String contents) { + vmData.add(new String[]{folder, file, contents}); + } + +} diff --git a/core/src/com/cloud/agent/api/storage/AbstractDownloadCommand.java b/core/src/com/cloud/agent/api/storage/AbstractDownloadCommand.java new file mode 100644 index 00000000000..e9a038f7cd6 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/AbstractDownloadCommand.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import com.cloud.storage.Storage.ImageFormat; + +public abstract class AbstractDownloadCommand extends StorageCommand { + + private String url; + private ImageFormat format; + private long accountId; + private String name; + + protected AbstractDownloadCommand() { + } + + protected AbstractDownloadCommand(String name, String url, ImageFormat format, long accountId) { + this.url = url; + this.format = format; + this.accountId = accountId; + this.name = name; + } + + protected AbstractDownloadCommand(AbstractDownloadCommand that) { + this(that.name, that.url, that.format, that.accountId); + } + + public String getUrl() { + return url; + } + + public String getName() { + return name; + } + + public ImageFormat getFormat() { + return format; + } + + public long getAccountId() { + return accountId; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public void setUrl(String url) { + this.url = url; + } + +} diff --git a/core/src/com/cloud/agent/api/storage/CopyVolumeAnswer.java b/core/src/com/cloud/agent/api/storage/CopyVolumeAnswer.java new file mode 100644 index 00000000000..fac03eaaccd --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/CopyVolumeAnswer.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +public class CopyVolumeAnswer extends Answer { + private String volumeFolder; + private String volumePath; + + protected CopyVolumeAnswer() { + } + + public CopyVolumeAnswer(Command command, boolean success, String details, String volumeFolder, String volumePath) { + super(command, success, details); + this.volumeFolder = volumeFolder; + this.volumePath = volumePath; + } + + public String getVolumeFolder() { + return volumeFolder; + } + + public String getVolumePath() { + return volumePath; + } +} diff --git a/core/src/com/cloud/agent/api/storage/CopyVolumeCommand.java b/core/src/com/cloud/agent/api/storage/CopyVolumeCommand.java new file mode 100644 index 00000000000..2e2c22e708a --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/CopyVolumeCommand.java @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Command; +import com.cloud.storage.StoragePoolVO; + +public class CopyVolumeCommand extends Command { + + long volumeId; + String volumePath; + StoragePoolVO pool; + String secondaryStorageURL; + boolean toSecondaryStorage; + + public CopyVolumeCommand() { + } + + public CopyVolumeCommand(long volumeId, String volumePath, StoragePoolVO pool, String secondaryStorageURL, boolean toSecondaryStorage) { + this.volumeId = volumeId; + this.volumePath = volumePath; + this.pool = pool; + this.secondaryStorageURL = secondaryStorageURL; + this.toSecondaryStorage = toSecondaryStorage; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String getVolumePath() { + return volumePath; + } + + public long getVolumeId() { + return volumeId; + } + + public StoragePoolVO getPool() { + return pool; + } + + public String getSecondaryStorageURL() { + return secondaryStorageURL; + } + + public boolean toSecondaryStorage() { + return toSecondaryStorage; + } +} diff --git a/core/src/com/cloud/agent/api/storage/CreateAnswer.java b/core/src/com/cloud/agent/api/storage/CreateAnswer.java new file mode 100644 index 00000000000..70c472bb2a1 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/CreateAnswer.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.VolumeTO; + +public class CreateAnswer extends Answer { + VolumeTO volume; + protected CreateAnswer() { + } + + public CreateAnswer(CreateCommand cmd, VolumeTO volume) { + super(cmd, true, null); + this.volume = volume; + } + + public CreateAnswer(CreateCommand cmd, String details) { + super(cmd, false, details); + } + + public CreateAnswer(CreateCommand cmd, Exception e) { + super(cmd, e); + } + + public VolumeTO getVolume() { + return volume; + } +} diff --git a/core/src/com/cloud/agent/api/storage/CreateCommand.java b/core/src/com/cloud/agent/api/storage/CreateCommand.java new file mode 100644 index 00000000000..2c0ade671a9 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/CreateCommand.java @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.DiskCharacteristicsTO; +import com.cloud.agent.api.to.StoragePoolTO; +import com.cloud.storage.StoragePoolVO; +import com.cloud.storage.VolumeVO; +import com.cloud.vm.VMInstanceVO; + +public class CreateCommand extends Command { + private long volId; + private StoragePoolTO pool; + private DiskCharacteristicsTO diskCharacteristics; + private String templateUrl; + private long size; + + protected CreateCommand() { + super(); + } + + /** + * Construction for template based volumes. + * + * @param vol + * @param vm + * @param diskCharacteristics + * @param templateUrl + * @param pool + */ + public CreateCommand(VolumeVO vol, VMInstanceVO vm, DiskCharacteristicsTO diskCharacteristics, String templateUrl, StoragePoolVO pool) { + this(vol, vm, diskCharacteristics, pool, 0); + this.templateUrl = templateUrl; + } + + /** + * Construction for regular volumes. + * + * @param vol + * @param vm + * @param diskCharacteristics + * @param pool + */ + public CreateCommand(VolumeVO vol, VMInstanceVO vm, DiskCharacteristicsTO diskCharacteristics, StoragePoolVO pool, long size) { + this.volId = vol.getId(); + this.diskCharacteristics = diskCharacteristics; + this.pool = new StoragePoolTO(pool); + this.templateUrl = null; + this.size = size; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String getTemplateUrl() { + return templateUrl; + } + + public StoragePoolTO getPool() { + return pool; + } + + public DiskCharacteristicsTO getDiskCharacteristics() { + return diskCharacteristics; + } + + public long getVolumeId() { + return volId; + } + + public long getSize(){ + return this.size; + } +} diff --git a/core/src/com/cloud/agent/api/storage/CreatePrivateTemplateAnswer.java b/core/src/com/cloud/agent/api/storage/CreatePrivateTemplateAnswer.java new file mode 100644 index 00000000000..352c3e1f1d1 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/CreatePrivateTemplateAnswer.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.storage.Storage.ImageFormat; + +public class CreatePrivateTemplateAnswer extends Answer { + private String _path; + private long _virtualSize; + private String _uniqueName; + private ImageFormat _format; + + public CreatePrivateTemplateAnswer() {} + + public CreatePrivateTemplateAnswer(Command cmd, boolean success, String result, String path, long virtualSize, String uniqueName, ImageFormat format) { + super(cmd, success, result); + _path = path; + _virtualSize = virtualSize; + _uniqueName = uniqueName; + _format = format; + } + + public String getPath() { + return _path; + } + + public void setPath(String path) { + _path = path; + } + + public long getVirtualSize() { + return _virtualSize; + } + + public void setVirtualSize(long virtualSize) { + _virtualSize = virtualSize; + } + + public String getUniqueName() { + return _uniqueName; + } + + public ImageFormat getImageFormat() { + return _format; + } +} diff --git a/core/src/com/cloud/agent/api/storage/CreatePrivateTemplateCommand.java b/core/src/com/cloud/agent/api/storage/CreatePrivateTemplateCommand.java new file mode 100644 index 00000000000..8f52a5ad97b --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/CreatePrivateTemplateCommand.java @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.storage; + +public class CreatePrivateTemplateCommand extends StorageCommand { + private String _snapshotFolder; + private String _snapshotPath; + private String _userFolder; + private String _userSpecifiedName; + private String _uniqueName; + private long _templateId; + private long _accountId; + + // For XenServer + private String _secondaryStorageURL; + private String _snapshotName; + + public CreatePrivateTemplateCommand() {} + + public CreatePrivateTemplateCommand(String secondaryStorageURL, long templateId, long accountId, String userSpecifiedName, String uniqueName, String snapshotFolder, String snapshotPath, String snapshotName, String userFolder) { + _secondaryStorageURL = secondaryStorageURL; + _templateId = templateId; + _accountId = accountId; + _userSpecifiedName = userSpecifiedName; + _uniqueName = uniqueName; + _snapshotFolder = snapshotFolder; + _snapshotPath = snapshotPath; + _snapshotName = snapshotName; + _userFolder = userFolder; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public String getSecondaryStorageURL() { + return _secondaryStorageURL; + } + + public String getTemplateName() { + return _userSpecifiedName; + } + + public String getUniqueName() { + return _uniqueName; + } + + public String getSnapshotFolder() { + return _snapshotFolder; + } + + public String getSnapshotPath() { + return _snapshotPath; + } + + public String getSnapshotName() { + return _snapshotName; + } + + public String getUserFolder() { + return _userFolder; + } + + public long getTemplateId() { + return _templateId; + } + + public long getAccountId() { + return _accountId; + } + + public void setTemplateId(long templateId) { + _templateId = templateId; + } +} diff --git a/core/src/com/cloud/agent/api/storage/DeleteTemplateCommand.java b/core/src/com/cloud/agent/api/storage/DeleteTemplateCommand.java new file mode 100644 index 00000000000..f65b07d755f --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/DeleteTemplateCommand.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Command; +import com.cloud.storage.StoragePoolVO; + +public class DeleteTemplateCommand extends Command { + + String templatePath; + + public DeleteTemplateCommand() { + } + + public DeleteTemplateCommand(String templatePath) { + this.templatePath = templatePath; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String getTemplatePath() { + return templatePath; + } + +} diff --git a/core/src/com/cloud/agent/api/storage/DestroyCommand.java b/core/src/com/cloud/agent/api/storage/DestroyCommand.java new file mode 100755 index 00000000000..fe8f9eeebfc --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/DestroyCommand.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.storage.StoragePoolVO; +import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.storage.VolumeVO; + +public class DestroyCommand extends StorageCommand { + VolumeTO volume; + + protected DestroyCommand() { + } + + public DestroyCommand(StoragePoolVO pool, VolumeVO volume) { + this.volume = new VolumeTO(volume, pool); + } + + public DestroyCommand(StoragePoolVO pool, VMTemplateStoragePoolVO templatePoolRef) { + this.volume = new VolumeTO(templatePoolRef, pool); + } + + public VolumeTO getVolume() { + return volume; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/com/cloud/agent/api/storage/DownloadAnswer.java b/core/src/com/cloud/agent/api/storage/DownloadAnswer.java new file mode 100644 index 00000000000..d542b14f9df --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/DownloadAnswer.java @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import java.io.File; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.storage.VMTemplateHostVO; +import com.cloud.storage.VMTemplateStorageResourceAssoc.Status; + +public class DownloadAnswer extends Answer { + private String jobId; + private int downloadPct; + private String errorString; + private VMTemplateHostVO.Status downloadStatus; + private String downloadPath; + private String installPath; + public Long templateSize = 0L; + + public int getDownloadPct() { + return downloadPct; + } + public String getErrorString() { + return errorString; + } + + public String getDownloadStatusString() { + return downloadStatus.toString(); + } + + public VMTemplateHostVO.Status getDownloadStatus() { + return downloadStatus; + } + + public String getDownloadPath() { + return downloadPath; + } + protected DownloadAnswer() { + + } + + public String getJobId() { + return jobId; + } + public void setJobId(String jobId) { + this.jobId = jobId; + } + + public DownloadAnswer(String jobId, int downloadPct, String errorString, + Status downloadStatus, String fileSystemPath, String installPath, long templateSize) { + super(); + this.jobId = jobId; + this.downloadPct = downloadPct; + this.errorString = errorString; + this.downloadStatus = downloadStatus; + this.downloadPath = fileSystemPath; + this.installPath = fixPath(installPath); + this.templateSize = templateSize; + } + + public DownloadAnswer(String jobId, int downloadPct, Command command, + Status downloadStatus, String fileSystemPath, String installPath) { + super(command); + this.jobId = jobId; + this.downloadPct = downloadPct; + this.downloadStatus = downloadStatus; + this.downloadPath = fileSystemPath; + this.installPath = installPath; + } + + private static String fixPath(String path){ + if (path == null) + return path; + if (path.startsWith(File.separator)) { + path=path.substring(File.separator.length()); + } + if (path.endsWith(File.separator)) { + path=path.substring(0, path.length()-File.separator.length()); + } + return path; + } + + public void setDownloadStatus(VMTemplateHostVO.Status downloadStatus) { + this.downloadStatus = downloadStatus; + } + + public String getInstallPath() { + return installPath; + } + public void setInstallPath(String installPath) { + this.installPath = fixPath(installPath); + } + + public void setTemplateSize(long templateSize) { + this.templateSize = templateSize; + } + + public Long getTemplateSize() { + return templateSize; + } + +} diff --git a/core/src/com/cloud/agent/api/storage/DownloadCommand.java b/core/src/com/cloud/agent/api/storage/DownloadCommand.java new file mode 100644 index 00000000000..ea5c37806f0 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/DownloadCommand.java @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.Storage.ImageFormat; + + +/** + * @author chiradeep + * + */ +public class DownloadCommand extends AbstractDownloadCommand { + public static class PasswordAuth { + String userName; + String password; + public PasswordAuth() { + + } + public PasswordAuth(String user, String password) { + this.userName = user; + this.password = password; + } + public String getUserName() { + return userName; + } + public String getPassword() { + return password; + } + } + private boolean hvm; + private String description; + private String checksum; + private PasswordAuth auth; + private Long maxDownloadSizeInBytes = null; + private long id; + + protected DownloadCommand() { + } + + + public DownloadCommand(DownloadCommand that) { + super(that); + this.hvm = that.hvm; + this.checksum = that.checksum; + this.id = that.id; + this.description = that.description; + this.auth = that.getAuth(); + this.maxDownloadSizeInBytes = that.getMaxDownloadSizeInBytes(); + } + + public DownloadCommand(VMTemplateVO template, Long maxDownloadSizeInBytes) { + super(template.getUniqueName(), template.getUrl(), template.getFormat(), template.getAccountId()); + this.hvm = template.requiresHvm(); + this.checksum = template.getChecksum(); + this.id = template.getId(); + this.description = template.getDisplayText(); + this.maxDownloadSizeInBytes = maxDownloadSizeInBytes; + } + + public DownloadCommand(String url, String name, ImageFormat format, boolean isHvm, Long accountId, Long templateId, String descr, String cksum, String user, String passwd, Long maxDownloadSizeInBytes) { + super(name, url, format, accountId); + this.setHvm(isHvm); + this.description = descr; + this.checksum = cksum; + this.id = templateId; + auth = new PasswordAuth(user, passwd); + this.maxDownloadSizeInBytes = maxDownloadSizeInBytes; + } + + public long getId() { + return id; + } + + public void setHvm(boolean hvm) { + this.hvm = hvm; + } + + public boolean isHvm() { + return hvm; + } + + public String getDescription() { + return description; + } + + public String getChecksum() { + return checksum; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setChecksum(String checksum) { + this.checksum = checksum; + } + + @Override + public boolean executeInSequence() { + return false; + } + + + public PasswordAuth getAuth() { + return auth; + } + + public void setCreds(String userName, String passwd) { + auth = new PasswordAuth(userName, passwd); + } + + public Long getMaxDownloadSizeInBytes() { + return maxDownloadSizeInBytes; + } +} diff --git a/core/src/com/cloud/agent/api/storage/DownloadProgressCommand.java b/core/src/com/cloud/agent/api/storage/DownloadProgressCommand.java new file mode 100644 index 00000000000..4426bcff271 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/DownloadProgressCommand.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + + + +/** + * @author chiradeep + * + */ +public class DownloadProgressCommand extends DownloadCommand { + public static enum RequestType {GET_STATUS, ABORT, RESTART, PURGE, GET_OR_RESTART} + private String jobId; + private RequestType request; + + protected DownloadProgressCommand() { + super(); + } + + public DownloadProgressCommand(DownloadCommand cmd, String jobId, RequestType req) { + super(cmd); + + this.jobId = jobId; + this.setRequest(req); + } + + public String getJobId() { + return jobId; + } + + public void setRequest(RequestType request) { + this.request = request; + } + + public RequestType getRequest() { + return request; + } +} diff --git a/core/src/com/cloud/agent/api/storage/ManageVolumeAvailabilityAnswer.java b/core/src/com/cloud/agent/api/storage/ManageVolumeAvailabilityAnswer.java new file mode 100644 index 00000000000..cf59790f1ad --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/ManageVolumeAvailabilityAnswer.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +public class ManageVolumeAvailabilityAnswer extends Answer { + + protected ManageVolumeAvailabilityAnswer() { + } + + public ManageVolumeAvailabilityAnswer(Command command, boolean success, String details) { + super(command, success, details); + } + +} diff --git a/core/src/com/cloud/agent/api/storage/ManageVolumeAvailabilityCommand.java b/core/src/com/cloud/agent/api/storage/ManageVolumeAvailabilityCommand.java new file mode 100644 index 00000000000..d4518f493a3 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/ManageVolumeAvailabilityCommand.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Command; + +public class ManageVolumeAvailabilityCommand extends Command { + + boolean attach; + String primaryStorageSRUuid; + String volumeUuid; + + + public ManageVolumeAvailabilityCommand() { + } + + public ManageVolumeAvailabilityCommand(boolean attach, String primaryStorageSRUuid, String volumeUuid) { + this.attach = attach; + this.primaryStorageSRUuid = primaryStorageSRUuid; + this.volumeUuid = volumeUuid; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public boolean getAttach() { + return attach; + } + + public String getPrimaryStorageSRUuid() { + return primaryStorageSRUuid; + } + + public String getVolumeUuid() { + return volumeUuid; + } + +} diff --git a/core/src/com/cloud/agent/api/storage/PrimaryStorageDownloadCommand.java b/core/src/com/cloud/agent/api/storage/PrimaryStorageDownloadCommand.java new file mode 100644 index 00000000000..a9010d5782e --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/PrimaryStorageDownloadCommand.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import com.cloud.storage.Storage.ImageFormat; + + +/** + * @author Anthony + * + */ +public class PrimaryStorageDownloadCommand extends AbstractDownloadCommand { + String localPath; + String poolUuid; + long poolId; + + protected PrimaryStorageDownloadCommand() { + } + + public PrimaryStorageDownloadCommand(String name, String url, ImageFormat format, long accountId, long poolId, String poolUuid) { + super(name, url, format, accountId); + this.poolId = poolId; + this.poolUuid = poolUuid; + } + + public String getPoolUuid() { + return poolUuid; + } + + public long getPoolId() { + return poolId; + } + + public void setLocalPath(String path) { + this.localPath = path; + } + + public String getLocalPath() { + return localPath; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/com/cloud/agent/api/storage/ShareAnswer.java b/core/src/com/cloud/agent/api/storage/ShareAnswer.java new file mode 100644 index 00000000000..1ddbdd33505 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/ShareAnswer.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import java.util.Map; + +import com.cloud.agent.api.Answer; + +public class ShareAnswer extends Answer { + + protected ShareAnswer() { + } + + Map mapping; + + public ShareAnswer(ShareCommand cmd, Map mapping) { + super(cmd, true, null); + this.mapping = mapping; + } + + public ShareAnswer(ShareCommand cmd, String details) { + super(cmd, false, details); + } + + public Map getMappings() { + return mapping; + } + +} diff --git a/core/src/com/cloud/agent/api/storage/ShareCommand.java b/core/src/com/cloud/agent/api/storage/ShareCommand.java new file mode 100644 index 00000000000..09ad9957594 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/ShareCommand.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import java.util.List; + +import com.cloud.storage.VolumeVO; + +public class ShareCommand extends StorageCommand { + public static String UnshareAll = "unshare_all"; + + private boolean share; + private boolean removePreviousShare; + private List volumes; + private String vmName; + private String initiatorIqn; + + protected ShareCommand() { + } + + public ShareCommand(String vmName, List vols, String initiatorIqn, boolean removePreviousShare) { + super(); + this.vmName = vmName; + this.initiatorIqn = initiatorIqn; + this.share = true; + this.volumes = vols; + this.removePreviousShare = removePreviousShare; + } + + public ShareCommand(String vmName, List vols, String initiatorIqn) { + super(); + this.vmName = vmName; + this.initiatorIqn = initiatorIqn; + this.share = false; + this.volumes = vols; + this.removePreviousShare = true; + } + + public ShareCommand(String vmName, List vols) { + super(); + this.vmName = vmName; + this.initiatorIqn = UnshareAll; + this.share = false; + this.volumes = vols; + this.removePreviousShare = true; + } + + // NOTE: We set this to false because we leave it up to the business logic + // to make sure it is already created before calling shared. + @Override + public boolean executeInSequence() { + return false; + } + + public boolean isShare() { + return share; + } + + public List getVolumes() { + return volumes; + } + + public String getVmName() { + return vmName; + } + + public String getInitiatorIqn() { + return initiatorIqn; + } + + public boolean removePreviousShare() { + return removePreviousShare; + } +} diff --git a/core/src/com/cloud/agent/api/storage/StorageCommand.java b/core/src/com/cloud/agent/api/storage/StorageCommand.java new file mode 100644 index 00000000000..30a7c73a0d1 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/StorageCommand.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Command; + +public abstract class StorageCommand extends Command { + protected StorageCommand() { + super(); + } + +} diff --git a/core/src/com/cloud/agent/api/storage/UpgradeDiskAnswer.java b/core/src/com/cloud/agent/api/storage/UpgradeDiskAnswer.java new file mode 100644 index 00000000000..aa14bf52b68 --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/UpgradeDiskAnswer.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +public class UpgradeDiskAnswer extends Answer { + + public UpgradeDiskAnswer() {} + + public UpgradeDiskAnswer(Command cmd, boolean success, String details) { + super(cmd, success, details); + } +} diff --git a/core/src/com/cloud/agent/api/storage/UpgradeDiskCommand.java b/core/src/com/cloud/agent/api/storage/UpgradeDiskCommand.java new file mode 100644 index 00000000000..fa2e7dc3f4a --- /dev/null +++ b/core/src/com/cloud/agent/api/storage/UpgradeDiskCommand.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.api.storage; + + +public class UpgradeDiskCommand extends StorageCommand { + + private String _imagePath; + private String _newSize; + + public UpgradeDiskCommand() {} + + public UpgradeDiskCommand(String imagePath, String newSize) { + _imagePath = imagePath; + _newSize = newSize; + } + + public String getImagePath() { + return _imagePath; + } + public void setImagePath(String imagePath) { + _imagePath = imagePath; + } + + public String getNewSize() { + return _newSize; + } + public void setNewSize(String newSize) { + _newSize = newSize; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/com/cloud/agent/api/to/DiskCharacteristicsTO.java b/core/src/com/cloud/agent/api/to/DiskCharacteristicsTO.java new file mode 100644 index 00000000000..ec4bf6f8482 --- /dev/null +++ b/core/src/com/cloud/agent/api/to/DiskCharacteristicsTO.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.to; + +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.Volume; + +public class DiskCharacteristicsTO { + private long size; + private String[] tags; + private Volume.VolumeType type; + private String name; + private boolean useLocalStorage; + private boolean recreatable; + + protected DiskCharacteristicsTO() { + } + + public DiskCharacteristicsTO(Volume.VolumeType type, String name, long size, String[] tags, boolean useLocalStorage, boolean recreatable) { + this.type = type; + this.name = name; + this.size = size; + this.tags = tags; + this.useLocalStorage = useLocalStorage; + this.recreatable = recreatable; + } + + public DiskCharacteristicsTO(Volume.VolumeType type, String name, DiskOfferingVO offering, long size) { + this(type, name, size, offering.getTagsArray(), offering.getUseLocalStorage(), offering.isRecreatable()); + } + + public DiskCharacteristicsTO(Volume.VolumeType type, String name, DiskOfferingVO offering) { + this(type, name, offering.getDiskSizeInBytes(), offering.getTagsArray(), offering.getUseLocalStorage(), offering.isRecreatable()); + } + + public long getSize() { + return size; + } + + public String getName() { + return name; + } + + public String[] getTags() { + return tags; + } + + public Volume.VolumeType getType() { + return type; + } + + public boolean useLocalStorage() { + return useLocalStorage; + } + + public boolean isRecreatable() { + return recreatable; + } + + @Override + public String toString() { + return new StringBuilder("DskChr[").append(type).append("|").append(size).append("|").append("]").toString(); + } +} diff --git a/core/src/com/cloud/agent/api/to/HostTO.java b/core/src/com/cloud/agent/api/to/HostTO.java new file mode 100644 index 00000000000..91cfa65437e --- /dev/null +++ b/core/src/com/cloud/agent/api/to/HostTO.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.to; + +import com.cloud.host.HostVO; + +public class HostTO { + private String guid; + private NetworkTO privateNetwork; + private NetworkTO publicNetwork; + private NetworkTO storageNetwork1; + private NetworkTO storageNetwork2; + + protected HostTO() { + } + + public HostTO(HostVO vo) { + guid = vo.getGuid(); + privateNetwork = new NetworkTO(vo.getPrivateIpAddress(), vo.getPrivateNetmask(), vo.getPrivateMacAddress()); + if (vo.getPublicIpAddress() != null) { + publicNetwork = new NetworkTO(vo.getPublicIpAddress(), vo.getPublicNetmask(), vo.getPublicMacAddress()); + } + if (vo.getStorageIpAddress() != null) { + storageNetwork1 = new NetworkTO(vo.getStorageIpAddress(), vo.getStorageNetmask(), vo.getStorageMacAddress()); + } + if (vo.getStorageIpAddressDeux() != null) { + storageNetwork2 = new NetworkTO(vo.getStorageIpAddressDeux(), vo.getStorageNetmaskDeux(), vo.getStorageMacAddressDeux()); + } + } + + public String getGuid() { + return guid; + } + + public void setGuid(String guid) { + this.guid = guid; + } + + public NetworkTO getPrivateNetwork() { + return privateNetwork; + } + + public void setPrivateNetwork(NetworkTO privateNetwork) { + this.privateNetwork = privateNetwork; + } + + public NetworkTO getPublicNetwork() { + return publicNetwork; + } + + public void setPublicNetwork(NetworkTO publicNetwork) { + this.publicNetwork = publicNetwork; + } + + public NetworkTO getStorageNetwork1() { + return storageNetwork1; + } + + public void setStorageNetwork1(NetworkTO storageNetwork1) { + this.storageNetwork1 = storageNetwork1; + } + + public NetworkTO getStorageNetwork2() { + return storageNetwork2; + } + + public void setStorageNetwork2(NetworkTO storageNetwork2) { + this.storageNetwork2 = storageNetwork2; + } +} diff --git a/core/src/com/cloud/agent/api/to/NetworkTO.java b/core/src/com/cloud/agent/api/to/NetworkTO.java new file mode 100644 index 00000000000..50cb1cd034c --- /dev/null +++ b/core/src/com/cloud/agent/api/to/NetworkTO.java @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.to; + +/** + * Transfer object to transfer network settings. + */ +public class NetworkTO { + private String ip; + private String netmask; + private String gateway; + private String mac; + private String dns1; + private String dns2; + private String vlan; + + protected NetworkTO() { + } + + /** + * This constructor is usually for hosts where the other information are not important. + * + * @param ip ip address + * @param netmask netmask + * @param mac mac address + */ + public NetworkTO(String ip, String netmask, String mac) { + this(ip, null, netmask, mac, null, null, null); + } + + /** + * This is the full constructor and should be used for VM's network as it contains + * the full information about what is needed. + * + * @param ip + * @param vlan + * @param netmask + * @param mac + * @param gateway + * @param dns1 + * @param dns2 + */ + public NetworkTO(String ip, String vlan, String netmask, String mac, String gateway, String dns1, String dns2) { + this.ip = ip; + this.netmask = netmask; + this.mac = mac; + this.gateway = gateway; + this.dns1 = dns1; + this.dns2 = dns2; + this.vlan = vlan; + } + + public String getIp() { + return ip; + } + + public String getNetmask() { + return netmask; + } + + public String getGateway() { + return gateway; + } + + public String getMac() { + return mac; + } + + public String getDns1() { + return dns1; + } + + public String getDns2() { + return dns2; + } +} diff --git a/core/src/com/cloud/agent/api/to/StoragePoolTO.java b/core/src/com/cloud/agent/api/to/StoragePoolTO.java new file mode 100644 index 00000000000..188dc1c8094 --- /dev/null +++ b/core/src/com/cloud/agent/api/to/StoragePoolTO.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.to; + +import com.cloud.storage.StoragePoolVO; +import com.cloud.storage.Storage.StoragePoolType; + + +public class StoragePoolTO { + long id; + String uuid; + String host; + String path; + int port; + StoragePoolType type; + + public StoragePoolTO(StoragePoolVO pool) { + this.id = pool.getId(); + this.host = pool.getHostAddress(); + this.port = pool.getPort(); + this.path = pool.getPath(); + this.type = pool.getPoolType(); + this.uuid = pool.getUuid(); + } + + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public String getHost() { + return host; + } + + public String getPath() { + return path; + } + + public int getPort() { + return port; + } + + public StoragePoolType getType() { + return type; + } + + protected StoragePoolTO() { + } + + @Override + public String toString() { + return new StringBuilder("Pool[").append(id).append("|").append(host).append(":").append(port).append("|").append(path).append("]").toString(); + } +} diff --git a/core/src/com/cloud/agent/api/to/TemplateTO.java b/core/src/com/cloud/agent/api/to/TemplateTO.java new file mode 100644 index 00000000000..02372598170 --- /dev/null +++ b/core/src/com/cloud/agent/api/to/TemplateTO.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.to; + +import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.Storage.ImageFormat; + +public class TemplateTO { + private long id; + private String uniqueName; + private ImageFormat format; + + protected TemplateTO() { + } + + public TemplateTO(VMTemplateVO template, VMTemplateStoragePoolVO storedAt) { + this.id = template.getId(); + this.uniqueName = template.getUniqueName(); + this.format = template.getFormat(); + } + + public long getId() { + return id; + } + + public String getUniqueName() { + return uniqueName; + } + + public ImageFormat getFormat() { + return format; + } + + @Override + public String toString() { + return new StringBuilder("Tmpl[").append(id).append("|").append(uniqueName).append("]").toString(); + } +} diff --git a/core/src/com/cloud/agent/api/to/VmTO.java b/core/src/com/cloud/agent/api/to/VmTO.java new file mode 100644 index 00000000000..7df5041def4 --- /dev/null +++ b/core/src/com/cloud/agent/api/to/VmTO.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.to; + +import com.cloud.vm.VMInstanceVO; + +public class VmTO { + private long id; + private String name; + NetworkTO[] networks; + + public VmTO() { + } + + // FIXME: Preferrably NetworkTO is constructed inside the TO objects. + // But we're press for time so I'm going to let the conversion + // happen outside. + public VmTO(VMInstanceVO instance, NetworkTO[] networks) { + id = instance.getId(); + name = instance.getName(); + this.networks = networks; + } + +} diff --git a/core/src/com/cloud/agent/api/to/VolumeTO.java b/core/src/com/cloud/agent/api/to/VolumeTO.java new file mode 100644 index 00000000000..e0afde2cb2e --- /dev/null +++ b/core/src/com/cloud/agent/api/to/VolumeTO.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.api.to; + +import com.cloud.storage.StoragePoolVO; +import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Volume.StorageResourceType; + + +public class VolumeTO { + protected VolumeTO() { + } + + private long id; + private String name; + private String mountPoint; + private String path; + private long size; + private Volume.VolumeType type; + private Volume.StorageResourceType resourceType; + private StoragePoolType storagePoolType; + private long poolId; + + public VolumeTO(long id, Volume.VolumeType type, Volume.StorageResourceType resourceType, StoragePoolType poolType, String name, String mountPoint, String path, long size) { + this.id = id; + this.name= name; + this.path = path; + this.size = size; + this.type = type; + this.resourceType = resourceType; + this.storagePoolType = poolType; + this.mountPoint = mountPoint; + } + + public VolumeTO(VolumeVO volume, StoragePoolVO pool) { + this.id = volume.getId(); + this.name = volume.getName(); + this.path = volume.getPath(); + this.size = volume.getSize(); + this.type = volume.getVolumeType(); + this.resourceType = volume.getStorageResourceType(); + this.storagePoolType = pool.getPoolType(); + this.mountPoint = volume.getFolder(); + } + + public VolumeTO(VMTemplateStoragePoolVO templatePoolRef, StoragePoolVO pool) { + this.id = templatePoolRef.getId(); + this.path = templatePoolRef.getInstallPath(); + this.size = templatePoolRef.getTemplateSize(); + this.resourceType = StorageResourceType.STORAGE_POOL; + this.storagePoolType = pool.getPoolType(); + this.mountPoint = pool.getPath(); + } + + public Volume.StorageResourceType getResourceType() { + return resourceType; + } + + public long getId() { + return id; + } + + public String getPath() { + return path; + } + + public long getSize() { + return size; + } + + public Volume.VolumeType getType() { + return type; + } + + public String getName() { + return name; + } + + public String getMountPoint() { + return mountPoint; + } + + public StoragePoolType getPoolType() { + return storagePoolType; + } + + @Override + public String toString() { + return new StringBuilder("Vol[").append(id).append("|").append(type).append("|").append(path).append("|").append(size).append("]").toString(); + } +} diff --git a/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java b/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java new file mode 100755 index 00000000000..0e9090f432d --- /dev/null +++ b/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java @@ -0,0 +1,616 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.resource.virtualnetwork; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetSocketAddress; +import java.net.URL; +import java.net.URLConnection; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; +import org.apache.commons.codec.binary.Base64; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.proxy.CheckConsoleProxyLoadCommand; +import com.cloud.agent.api.proxy.ConsoleProxyLoadAnswer; +import com.cloud.agent.api.proxy.WatchConsoleProxyLoadCommand; +import com.cloud.agent.api.routing.DhcpEntryCommand; +import com.cloud.agent.api.routing.IPAssocCommand; +import com.cloud.agent.api.routing.LoadBalancerCfgCommand; +import com.cloud.agent.api.routing.SavePasswordCommand; +import com.cloud.agent.api.routing.SetFirewallRuleCommand; +import com.cloud.agent.api.routing.VmDataCommand; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.component.Manager; +import com.cloud.utils.net.NetUtils; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; + +/** + * VirtualNetworkResource controls and configures virtual networking + * + * @config + * {@table + * || Param Name | Description | Values | Default || + * } + **/ +@Local(value={VirtualRoutingResource.class}) +public class VirtualRoutingResource implements Manager { + private static final Logger s_logger = Logger.getLogger(VirtualRoutingResource.class); + private String _savepasswordPath; // This script saves a random password to the DomR file system + private String _ipassocPath; + private String _publicIpAddress; + private String _firewallPath; + private String _loadbPath; + private String _dhcpEntryPath; + private String _vmDataPath; + private String _publicEthIf; + private String _privateEthIf; + + + private int _timeout; + private int _startTimeout; + private String _scriptsDir; + private String _name; + private int _sleep; + private int _retry; + private int _port; + + public Answer executeRequest(final Command cmd) { + try { + if (cmd instanceof SetFirewallRuleCommand) { + return execute((SetFirewallRuleCommand)cmd); + }else if (cmd instanceof LoadBalancerCfgCommand) { + return execute((LoadBalancerCfgCommand)cmd); + } else if (cmd instanceof IPAssocCommand) { + return execute((IPAssocCommand)cmd); + } else if (cmd instanceof CheckConsoleProxyLoadCommand) { + return execute((CheckConsoleProxyLoadCommand)cmd); + } else if(cmd instanceof WatchConsoleProxyLoadCommand) { + return execute((WatchConsoleProxyLoadCommand)cmd); + } else if (cmd instanceof SavePasswordCommand) { + return execute((SavePasswordCommand)cmd); + } else if (cmd instanceof DhcpEntryCommand) { + return execute((DhcpEntryCommand)cmd); + } else if (cmd instanceof VmDataCommand) { + return execute ((VmDataCommand)cmd); + } else { + return Answer.createUnsupportedCommandAnswer(cmd); + } + } catch (final IllegalArgumentException e) { + return new Answer(cmd, false, e.getMessage()); + } + } + + protected Answer execute(VmDataCommand cmd) { + List vmData = cmd.getVmData(); + + for (String[] vmDataEntry : vmData) { + String folder = vmDataEntry[0]; + String file = vmDataEntry[1]; + String data = vmDataEntry[2]; + File tmpFile = null; + + byte[] dataBytes = null; + if (data != null) { + if (folder.equals("userdata")) { + dataBytes = Base64.decodeBase64(data);//userdata is supplied in url-safe unchunked mode + } else { + dataBytes = data.getBytes(); + } + + try { + tmpFile = File.createTempFile("vmdata_", null); + FileOutputStream outStream = new FileOutputStream(tmpFile); + outStream.write(dataBytes); + outStream.close(); + } catch (IOException e) { + String tmpDir = System.getProperty("java.io.tmpdir"); + s_logger.warn("Failed to create temporary file: is " + tmpDir + " full?", e); + return new Answer(cmd, false, "Failed to create or write to temporary file: is " + tmpDir + " full? " + e.getMessage() ); + } + } + + final Script command = new Script(_vmDataPath, _timeout, s_logger); + command.add("-r", cmd.getRouterPrivateIpAddress()); + command.add("-v", cmd.getVmIpAddress()); + command.add("-F", folder); + command.add("-f", file); + + if (tmpFile != null) { + command.add("-d", tmpFile.getAbsolutePath()); + } + + final String result = command.execute(); + + if (tmpFile != null) { + boolean deleted = tmpFile.delete(); + if (!deleted) { + s_logger.warn("Failed to clean up temp file after sending vmdata"); + tmpFile.deleteOnExit(); + } + } + + if (result != null) { + return new Answer(cmd, false, result); + } + + } + + return new Answer(cmd); + } + + protected Answer execute(final LoadBalancerCfgCommand cmd) { + + File tmpCfgFile = null; + try { + String cfgFilePath = ""; + String routerIP = null; + + if (cmd.getRouterIp() != null) { + tmpCfgFile = File.createTempFile(cmd.getRouterIp().replace('.', '_'), "cfg"); + final PrintWriter out + = new PrintWriter(new BufferedWriter(new FileWriter(tmpCfgFile))); + for (int i=0; i < cmd.getConfig().length; i++) { + out.println(cmd.getConfig()[i]); + } + out.close(); + cfgFilePath = tmpCfgFile.getAbsolutePath(); + routerIP = cmd.getRouterIp(); + } + + final String result = setLoadBalancerConfig(cfgFilePath, + cmd.getAddFwRules(), cmd.getRemoveFwRules(), + routerIP); + + return new Answer(cmd, result == null, result); + } catch (final IOException e) { + return new Answer(cmd, false, e.getMessage()); + } finally { + if (tmpCfgFile != null) { + tmpCfgFile.delete(); + } + } + } + + protected Answer execute(final IPAssocCommand cmd) { + final String result = assignPublicIpAddress(cmd.getRouterName(), cmd.getRouterIp(), cmd.getPublicIp(), cmd.isAdd(), cmd.isSourceNat(), cmd.getVlanId(), cmd.getVlanGateway(), cmd.getVlanNetmask()); + if (result != null) { + return new Answer(cmd, false, result); + } + return new Answer(cmd); + } + + private String setLoadBalancerConfig(final String cfgFile, + final String[] addRules, final String[] removeRules, String routerIp) { + + if (routerIp == null) routerIp = "none"; + + final Script command = new Script(_loadbPath, _timeout, s_logger); + + command.add("-i", routerIp); + command.add("-f", cfgFile); + + StringBuilder sb = new StringBuilder(); + if (addRules.length > 0) { + for (int i=0; i< addRules.length; i++) { + sb.append(addRules[i]).append(','); + } + command.add("-a", sb.toString()); + } + + sb = new StringBuilder(); + if (removeRules.length > 0) { + for (int i=0; i< removeRules.length; i++) { + sb.append(removeRules[i]).append(','); + } + command.add("-d", sb.toString()); + } + + return command.execute(); + } + + protected synchronized Answer execute(final SavePasswordCommand cmd) { + final String password = cmd.getPassword(); + final String routerPrivateIPAddress = cmd.getRouterPrivateIpAddress(); + final String vmName = cmd.getVmName(); + final String vmIpAddress = cmd.getVmIpAddress(); + final String local = vmName; + + // Run save_password_to_domr.sh + final String result = savePassword(routerPrivateIPAddress, vmIpAddress, password, local); + if (result != null) { + return new Answer(cmd, false, "Unable to save password to DomR."); + } else { + return new Answer(cmd); + } + } + + protected synchronized Answer execute (final DhcpEntryCommand cmd) { + final Script command = new Script(_dhcpEntryPath, _timeout, s_logger); + command.add("-r", cmd.getRouterPrivateIpAddress()); + command.add("-v", cmd.getVmIpAddress()); + command.add("-m", cmd.getVmMac()); + command.add("-n", cmd.getVmName()); + + final String result = command.execute(); + return new Answer(cmd, result==null, result); + } + + protected Answer execute(final CheckConsoleProxyLoadCommand cmd) { + return executeProxyLoadScan(cmd, cmd.getProxyVmId(), cmd.getProxyVmName(), cmd.getProxyManagementIp(), cmd.getProxyCmdPort()); + } + + protected Answer execute(final WatchConsoleProxyLoadCommand cmd) { + return executeProxyLoadScan(cmd, cmd.getProxyVmId(), cmd.getProxyVmName(), cmd.getProxyManagementIp(), cmd.getProxyCmdPort()); + } + + private Answer executeProxyLoadScan(final Command cmd, final long proxyVmId, final String proxyVmName, final String proxyManagementIp, final int cmdPort) { + String result = null; + + final StringBuffer sb = new StringBuffer(); + sb.append("http://").append(proxyManagementIp).append(":" + cmdPort).append("/cmd/getstatus"); + + boolean success = true; + try { + final URL url = new URL(sb.toString()); + final URLConnection conn = url.openConnection(); + + final InputStream is = conn.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + final StringBuilder sb2 = new StringBuilder(); + String line = null; + try { + while ((line = reader.readLine()) != null) + sb2.append(line + "\n"); + result = sb2.toString(); + } catch (final IOException e) { + success = false; + } finally { + try { + is.close(); + } catch (final IOException e) { + s_logger.warn("Exception when closing , console proxy address : " + proxyManagementIp); + success = false; + } + } + } catch(final IOException e) { + s_logger.warn("Unable to open console proxy command port url, console proxy address : " + proxyManagementIp); + success = false; + } + + return new ConsoleProxyLoadAnswer(cmd, proxyVmId, proxyVmName, success, result); + } + + + + + public synchronized String savePassword(final String privateIpAddress, final String vmIpAddress, final String password, final String localPath) { + final Script command = new Script(_savepasswordPath, _startTimeout, s_logger); + command.add("-r", privateIpAddress); + command.add("-v", vmIpAddress); + command.add("-p", password); + command.add(localPath); + + return command.execute(); + } + + + public String assignPublicIpAddress(final String vmName, final long id, final String vnet, final String privateIpAddress, final String macAddress, final String publicIpAddress) { + + final Script command = new Script(_ipassocPath, _timeout, s_logger); + command.add("-A"); + command.add("-f"); //first ip is source nat ip + command.add("-r", vmName); + command.add("-i", privateIpAddress); + command.add("-a", macAddress); + command.add("-l", publicIpAddress); + + return command.execute(); + } + + public String assignPublicIpAddress(final String vmName, final String privateIpAddress, final String publicIpAddress, final boolean add, final boolean sourceNat, final String vlanId, final String vlanGateway, final String vlanNetmask) { + + final Script command = new Script(_ipassocPath, _timeout, s_logger); + if (add) { + command.add("-A"); + } else { + command.add("-D"); + } + if (sourceNat) { + command.add("-f"); + } + command.add("-i", privateIpAddress); + command.add("-l", publicIpAddress); + command.add("-r", vmName); + + command.add("-n", vlanNetmask); + + command.add("-c", "eth2"); + + if (vlanId != null) { + command.add("-v", vlanId); + command.add("-g", vlanGateway); + } + + return command.execute(); + } + + public String setFirewallRules(final boolean enable, final String routerName, final String routerIpAddress, final String protocol, + final String publicIpAddress, final String publicPort, final String privateIpAddress, final String privatePort, + String oldPrivateIP, String oldPrivatePort, String vlanNetmask) { + + if (routerIpAddress == null) { + s_logger.warn("SetFirewallRuleCommand did nothing because Router IP address was null when creating rule for public IP: " + publicIpAddress); + return null; + } + + if (oldPrivateIP == null) oldPrivateIP = ""; + if (oldPrivatePort == null) oldPrivatePort = ""; + + final Script command = new Script(_firewallPath, _timeout, s_logger); + + command.add(enable ? "-A" : "-D"); + command.add("-P", protocol); + command.add("-l", publicIpAddress); + command.add("-p", publicPort); + command.add("-n", routerName); + command.add("-i", routerIpAddress); + command.add("-r", privateIpAddress); + command.add("-d", privatePort); + command.add("-N", vlanNetmask); + command.add("-w", oldPrivateIP); + command.add("-x", oldPrivatePort); + + return command.execute(); + } + + private boolean isBridgeExists(String bridgeName) { + Script command = new Script("/bin/sh", _timeout); + command.add("-c"); + command.add("brctl show|grep " + bridgeName); + final OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + String result = command.execute(parser); + if (result != null || parser.getLine() == null) { + return false; + } else { + return true; + } + } + + private void deleteBridge(String brName) { + Script cmd = new Script("/bin/sh", _timeout); + cmd.add("-c"); + cmd.add("ifconfig " + brName + " down;brctl delbr " + brName); + cmd.execute(); + } + + private boolean isDNSmasqRunning(String dnsmasqName) { + Script cmd = new Script("/bin/sh", _timeout); + cmd.add("-c"); + cmd.add("ls -l /var/run/libvirt/network/" + dnsmasqName + ".pid"); + String result = cmd.execute(); + if (result != null) { + return false; + } else + return true; + } + + private void stopDnsmasq(String dnsmasqName) { + Script cmd = new Script("/bin/sh", _timeout); + cmd.add("-c"); + cmd.add("kill -9 `cat /var/run/libvirt/network/" + dnsmasqName +".pid`"); + cmd.execute(); + } + + public void cleanupPrivateNetwork(String privNwName, String privBrName){ + if (isDNSmasqRunning(privNwName)) { + stopDnsmasq(privNwName); + } + if (isBridgeExists(privBrName)) { + deleteBridge(privBrName); + } + + + } + + protected Answer execute(final SetFirewallRuleCommand cmd) { + final String result = setFirewallRules(cmd.isEnable(), + cmd.getRouterName(), + cmd.getRouterIpAddress(), + cmd.getProtocol().toLowerCase(), + cmd.getPublicIpAddress(), + cmd.getPublicPort(), + cmd.getPrivateIpAddress(), + cmd.getPrivatePort(), + cmd.getOldPrivateIP(), + cmd.getOldPrivatePort(), + cmd.getVlanNetmask()); + + return new Answer(cmd, result == null, result); + } + + protected String getDefaultScriptsDir() { + return "scripts/network/domr/dom0"; + } + + protected String findScript(final String script) { + return Script.findScript(_scriptsDir, script); + } + + @Override + public boolean configure(final String name, final Map params) throws ConfigurationException { + _name = name; + + _scriptsDir = (String)params.get("domr.scripts.dir"); + if (_scriptsDir == null) { + if(s_logger.isInfoEnabled()) + s_logger.info("VirtualRoutingResource _scriptDir can't be initialized from domr.scripts.dir param, use default" ); + _scriptsDir = getDefaultScriptsDir(); + } + + if(s_logger.isInfoEnabled()) + s_logger.info("VirtualRoutingResource _scriptDir to use: " + _scriptsDir); + + String value = (String)params.get("scripts.timeout"); + _timeout = NumbersUtil.parseInt(value, 120) * 1000; + + value = (String)params.get("start.script.timeout"); + _startTimeout = NumbersUtil.parseInt(value, 360) * 1000; + + value = (String)params.get("ssh.sleep"); + _sleep = NumbersUtil.parseInt(value, 5) * 1000; + + value = (String)params.get("ssh.retry"); + _retry = NumbersUtil.parseInt(value, 24); + + value = (String)params.get("ssh.port"); + _port = NumbersUtil.parseInt(value, 3922); + + _ipassocPath = findScript("ipassoc.sh"); + if (_ipassocPath == null) { + throw new ConfigurationException("Unable to find the ipassoc.sh"); + } + s_logger.info("ipassoc.sh found in " + _ipassocPath); + + _publicIpAddress = (String)params.get("public.ip.address"); + if (_publicIpAddress != null) { + s_logger.warn("Incoming public ip address is overriden. Will always be using the same ip address: " + _publicIpAddress); + } + + _firewallPath = findScript("firewall.sh"); + if (_firewallPath == null) { + throw new ConfigurationException("Unable to find the firewall.sh"); + } + + _loadbPath = findScript("loadbalancer.sh"); + if (_loadbPath == null) { + throw new ConfigurationException("Unable to find the loadbalancer.sh"); + } + + _savepasswordPath = findScript("save_password_to_domr.sh"); + if(_savepasswordPath == null) { + throw new ConfigurationException("Unable to find save_password_to_domr.sh"); + } + + _dhcpEntryPath = findScript("dhcp_entry.sh"); + if(_dhcpEntryPath == null) { + throw new ConfigurationException("Unable to find dhcp_entry.sh"); + } + + _vmDataPath = findScript("vm_data.sh"); + if(_vmDataPath == null) { + throw new ConfigurationException("Unable to find user_data.sh"); + } + + _publicEthIf = (String)params.get("public.network.device"); + if (_publicEthIf == null) { + _publicEthIf = "xenbr1"; + } + _publicEthIf = _publicEthIf.toLowerCase(); + + _privateEthIf = (String)params.get("private.network.device"); + if (_privateEthIf == null) { + _privateEthIf = "xenbr0"; + } + _privateEthIf = _privateEthIf.toLowerCase(); + + return true; + } + + + public String connect(final String ipAddress) { + return connect(ipAddress, _port); + } + + public String connect(final String ipAddress, final int port) { + for (int i = 0; i <= _retry; i++) { + SocketChannel sch = null; + try { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Trying to connect to " + ipAddress); + } + sch = SocketChannel.open(); + sch.configureBlocking(true); + + final InetSocketAddress addr = new InetSocketAddress(ipAddress, port); + sch.connect(addr); + return null; + } catch (final IOException e) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Could not connect to " + ipAddress); + } + } finally { + if (sch != null) { + try { + sch.close(); + } catch (final IOException e) {} + } + } + try { + Thread.sleep(_sleep); + } catch (final InterruptedException e) { + } + } + + s_logger.debug("Unable to logon to " + ipAddress); + + return "Unable to connect"; + } + + + @Override + public String getName() { + return _name; + } + + + + @Override + public boolean start() { + return true; + } + + + + @Override + public boolean stop() { + return true; + } + + +} + + diff --git a/core/src/com/cloud/agent/transport/ArrayTypeAdaptor.java b/core/src/com/cloud/agent/transport/ArrayTypeAdaptor.java new file mode 100755 index 00000000000..fdabc6fb666 --- /dev/null +++ b/core/src/com/cloud/agent/transport/ArrayTypeAdaptor.java @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.agent.transport; + +import java.lang.reflect.Array; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.cloud.agent.api.Command; +import com.cloud.storage.VolumeVO; +import com.cloud.utils.exception.CloudRuntimeException; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.reflect.TypeToken; + +public class ArrayTypeAdaptor implements JsonDeserializer, JsonSerializer { + + static final GsonBuilder s_gBuilder; + static { + s_gBuilder = new GsonBuilder(); + final Type listType = new TypeToken>() {}.getType(); + s_gBuilder.registerTypeAdapter(listType, new VolListTypeAdaptor()); + } + + + private static final String s_pkg = Command.class.getPackage().getName() + "."; + public ArrayTypeAdaptor() { + } + + public JsonElement serialize(T[] src, Type typeOfSrc, JsonSerializationContext context) { + Gson gson = s_gBuilder.create(); + JsonArray array = new JsonArray(); + for (T cmd : src) { + String result = gson.toJson(cmd); + array.add(new JsonPrimitive(cmd.getClass().getName().substring(s_pkg.length()))); + array.add(new JsonPrimitive(result)); + } + + return array; + } + + @SuppressWarnings("unchecked") + public T[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + JsonArray array = json.getAsJsonArray(); + Iterator it = array.iterator(); + ArrayList cmds = new ArrayList(); + Gson gson = Request.initBuilder().create(); + while (it.hasNext()) { + JsonElement element = it.next(); + String name = s_pkg + element.getAsString(); + Class clazz; + try { + clazz = Class.forName(name); + } catch (ClassNotFoundException e) { + throw new CloudRuntimeException("can't find " + name); + } + T cmd = (T)gson.fromJson(it.next().getAsString(), clazz); + cmds.add(cmd); + } + Class type = ((Class)typeOfT).getComponentType(); + T[] ts = (T[])Array.newInstance(type, cmds.size()); + return cmds.toArray(ts); + } +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/transport/Request.java b/core/src/com/cloud/agent/transport/Request.java new file mode 100755 index 00000000000..8267ad84799 --- /dev/null +++ b/core/src/com/cloud/agent/transport/Request.java @@ -0,0 +1,391 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.transport; + +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.util.List; + +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.exception.UnsupportedVersionException; +import com.cloud.storage.VolumeVO; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.exception.CloudRuntimeException; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +/** + * Request is a simple wrapper around command and answer to add sequencing, + * versioning, and flags. Note that the version here represents the changes + * in the over the wire protocol. For example, if we decide to not use Gson. + * It does not version the changes in the actual commands. That's expected + * to be done by adding new classes to the command and answer list. + * + * A request looks as follows: + * 1. Version - 1 byte; + * 2. Flags - 3 bytes; + * 3. Sequence - 8 bytes; + * 4. Length - 4 bytes; + * 5. ManagementServerId - 8 bytes; + * 6. AgentId - 8 bytes; + * 7. Data Package. + * + * Currently flags has only if it is a request or response. + */ +public class Request { + private static final Logger s_logger = Logger.getLogger(Request.class); + + public enum Version { + v1, // using gson to marshall + v2, // now using gson as marshalled. + v3; // Adding routing information into the Request data structure. + + public static Version get(final byte ver) throws UnsupportedVersionException { + for (final Version version : Version.values()) { + if (ver == version.ordinal()) + return version; + } + throw new UnsupportedVersionException("Can't lookup version: " + ver, UnsupportedVersionException.UnknownVersion); + } + }; + + protected static final short FLAG_RESPONSE = 0x0; + protected static final short FLAG_REQUEST = 0x1; + protected static final short FLAG_STOP_ON_ERROR = 0x2; + protected static final short FLAG_IN_SEQUENCE = 0x4; + protected static final short FLAG_WATCH = 0x8; + protected static final short FLAG_UPDATE = 0x10; + protected static final short FLAG_FROM_SERVER = 0x20; + protected static final short FLAG_CONTROL = 0x40; + + protected static final GsonBuilder s_gBuilder; + static { + s_gBuilder = new GsonBuilder(); + s_gBuilder.registerTypeAdapter(Command[].class, new ArrayTypeAdaptor()); + s_gBuilder.registerTypeAdapter(Answer[].class, new ArrayTypeAdaptor()); + final Type listType = new TypeToken>() {}.getType(); + s_gBuilder.registerTypeAdapter(listType, new VolListTypeAdaptor()); + s_logger.info("Builder inited."); + } + + public static GsonBuilder initBuilder() { + return s_gBuilder; + } + + protected Version _ver; + protected long _seq; + protected Command[] _cmds; + protected boolean _inSequence; + protected boolean _stopOnError; + protected boolean _fromServer; + protected boolean _control; + protected long _mgmtId; + protected long _agentId; + protected String _content; + + public Request(long seq, long agentId, long mgmtId, final Command command, boolean fromServer) { + this(seq, agentId, mgmtId, new Command[] {command}, true, fromServer); + } + + public Request(long seq, long agentId, long mgmtId, final Command[] commands, boolean fromServer) { + this(seq, agentId, mgmtId, commands, true, fromServer); + } + + protected Request(Version ver, long seq, long agentId, long mgmtId, final Command[] cmds, final Boolean inSequence, final boolean stopOnError, boolean fromServer) { + _ver = ver; + _cmds = cmds; + _stopOnError = stopOnError; + if (inSequence != null) { + _inSequence = inSequence; + } else { + for (final Command cmd : cmds) { + if (cmd.executeInSequence()) { + _inSequence = true; + break; + } + } + } + _seq = seq; + _agentId = agentId; + _mgmtId = mgmtId; + _fromServer = fromServer; + } + + protected Request(Version ver, long seq, long agentId, long mgmtId, final String content, final boolean inSequence, final boolean stopOnError, final boolean fromServer, final boolean control) { + _ver = ver; + _cmds = null; + _content = content; + _stopOnError = stopOnError; + _inSequence = inSequence; + _seq = seq; + _agentId = agentId; + _mgmtId = mgmtId; + _fromServer = fromServer; + _control = control; + } + + public Request(long seq, long agentId, long mgmtId, final Command[] cmds, final boolean stopOnError, boolean fromServer) { + this(Version.v3, seq, agentId, mgmtId, cmds, null, stopOnError, fromServer); + } + + public boolean isControl() { + return _control; + } + + public void setControl() { + _control = true; + } + + public long getManagementServerId() { + return _mgmtId; + } + + protected Request(final Request that, final Command[] cmds) { + this._ver = that._ver; + this._seq = that._seq; + this._inSequence = that._inSequence; + this._stopOnError = that._stopOnError; + this._cmds = cmds; + this._mgmtId = that._mgmtId; + this._agentId = that._agentId; + this._fromServer = !that._fromServer; + } + + protected Request() { + } + + public Version getVersion() { + return _ver; + } + + public void setAgentId(long agentId) { + _agentId = agentId; + } + + public boolean executeInSequence() { + return _inSequence; + } + + public long getSequence() { + return _seq; + } + + public boolean stopOnError() { + return _stopOnError; + } + + public Command getCommand() { + getCommands(); + return _cmds[0]; + } + + public Command[] getCommands() { + if (_cmds == null) { + final Gson json = s_gBuilder.create(); + _cmds = json.fromJson(_content, Command[].class); + } + return _cmds; + } + + /** + * Use this only surrounded by debug. + */ + @Override + public String toString() { + String content = _content; + if (content == null) { + final Gson gson = s_gBuilder.create(); + content = gson.toJson(_cmds); + } + final StringBuilder buffer = new StringBuilder(); + buffer.append("{ ").append(getType()); + buffer.append(", Seq: ").append(_seq).append(", Ver: ").append(_ver.toString()).append(", MgmtId: ").append(_mgmtId).append(", AgentId: ").append(_agentId).append(", Flags: ").append(Integer.toBinaryString(getFlags())); + buffer.append(", ").append(content).append(" }"); + return buffer.toString(); + } + + protected String getType() { + return "Cmd "; + } + + protected ByteBuffer serializeHeader(final int contentSize) { + final ByteBuffer buffer = ByteBuffer.allocate(32); + buffer.put(getVersionInByte()); + buffer.put((byte)0); + buffer.putShort(getFlags()); + buffer.putLong(_seq); + buffer.putInt(contentSize); + buffer.putLong(_mgmtId); + buffer.putLong(_agentId); + buffer.flip(); + + return buffer; + } + + public ByteBuffer[] toBytes() { + final Gson gson = s_gBuilder.create(); + final ByteBuffer[] buffers = new ByteBuffer[2]; + + if (_content == null) { + _content = gson.toJson(_cmds, _cmds.getClass()); + } + buffers[1] = ByteBuffer.wrap(_content.getBytes()); + buffers[0] = serializeHeader(buffers[1].capacity()); + + return buffers; + } + + public byte[] getBytes() { + final ByteBuffer[] buffers = toBytes(); + final int len1 = buffers[0].remaining(); + final int len2 = buffers[1].remaining(); + final byte[] bytes = new byte[len1 + len2]; + buffers[0].get(bytes, 0, len1); + buffers[1].get(bytes, len1, len2); + return bytes; + } + + protected byte getVersionInByte() { + return (byte)_ver.ordinal(); + } + + protected short getFlags() { + short flags = 0; + if (!(this instanceof Response)) { + flags = FLAG_REQUEST; + } else { + flags = FLAG_RESPONSE; + } + if (_inSequence) { + flags = (short)(flags | FLAG_IN_SEQUENCE); + } + if (_stopOnError) { + flags = (short)(flags | FLAG_STOP_ON_ERROR); + } + if (_fromServer) { + flags = (short)(flags | FLAG_FROM_SERVER); + } + if (_control) { + flags = (short)(flags | FLAG_CONTROL); + } + return flags; + } + + /** + * Factory method for Request and Response. It expects the bytes to be + * correctly formed so it's possible that it throws underflow exceptions + * but you shouldn't be concerned about that since that all bytes sent in + * should already be formatted correctly. + * + * @param bytes bytes to be converted. + * @return Request or Response depending on the data. + * @throws ClassNotFoundException if the Command or Answer can not be formed. + * @throws + */ + public static Request parse(final byte[] bytes) throws ClassNotFoundException, UnsupportedVersionException { + final ByteBuffer buff = ByteBuffer.wrap(bytes); + final byte ver = buff.get(); + final Version version = Version.get(ver); + if (version.ordinal() < Version.v3.ordinal()) { + throw new UnsupportedVersionException("This version is no longer supported: " + version.toString(), UnsupportedVersionException.IncompatibleVersion); + } + final byte reserved = buff.get(); // tossed away for now. + final Short flags = buff.getShort(); + final boolean isRequest = (flags & FLAG_REQUEST) > 0; + final boolean isControl = (flags & FLAG_IN_SEQUENCE) > 0; + final boolean isStopOnError = (flags & FLAG_STOP_ON_ERROR) > 0; + final boolean isWatch = (flags & FLAG_WATCH) > 0; + final boolean fromServer = (flags & FLAG_FROM_SERVER) > 0; + final boolean needsUpdate = (flags & FLAG_UPDATE) > 0; + final boolean control = (flags & FLAG_CONTROL) > 0; + + final long seq = buff.getLong(); + final int size = buff.getInt(); + final long mgmtId = buff.getLong(); + final long agentId = buff.getLong(); + + byte[] command = null; + int offset = 0; + if (buff.hasArray()) { + command = buff.array(); + offset = buff.arrayOffset() + buff.position(); + } else { + command = new byte[buff.remaining()]; + buff.get(command); + offset = 0; + } + + final String content = new String(command, offset, command.length - offset); + if (needsUpdate && !isRequest) { + return new UpgradeResponse(Version.get(ver), seq, content); + } + + if (isRequest) { + return new Request(version, seq, agentId, mgmtId, content, isControl, isStopOnError, fromServer, control); + } else { + return new Response(Version.get(ver), seq, agentId, mgmtId, content, isControl, isStopOnError, fromServer, control); + } + } + + public long getAgentId() { + return _agentId; + } + + public static boolean requiresSequentialExecution(final byte[] bytes) { + return (bytes[3] & FLAG_IN_SEQUENCE) > 0; + } + + public static Version getVersion(final byte[] bytes) throws UnsupportedVersionException { + try { + return Version.get(bytes[0]); + } catch (UnsupportedVersionException e) { + throw new CloudRuntimeException("Unsupported version: " + bytes[0]); + } + } + + public static long getManagementServerId(final byte[] bytes) { + return NumbersUtil.bytesToLong(bytes, 16); + } + + public static long getAgentId(final byte[] bytes) { + return NumbersUtil.bytesToLong(bytes, 24); + } + + public static boolean fromServer(final byte[] bytes) { + // int flags = NumbersUtil.bytesToShort(bytes, 2); + + return (bytes[3] & FLAG_FROM_SERVER) > 0; + } + + public static boolean isRequest(final byte[] bytes) { + return (bytes[3] & FLAG_REQUEST) > 0; + } + + public static long getSequence(final byte[] bytes) { + return NumbersUtil.bytesToLong(bytes, 4); + } + + public static boolean isControl(final byte[] bytes) { +// int flags = NumbersUtil.bytesToShort(bytes, 2); + return (bytes[3] & FLAG_CONTROL) > 0; + } +} diff --git a/core/src/com/cloud/agent/transport/Response.java b/core/src/com/cloud/agent/transport/Response.java new file mode 100755 index 00000000000..59eb2263810 --- /dev/null +++ b/core/src/com/cloud/agent/transport/Response.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.transport; + +import com.cloud.agent.api.Answer; +import com.cloud.exception.UnsupportedVersionException; +import com.google.gson.Gson; + +/** + * + */ +public class Response extends Request { + protected Response() { + } + + public Response(Request request, Answer answer) { + this(request, new Answer[] { answer }); + } + + public Response(Request request, Answer answer, long mgmtId, long agentId) { + this(request, new Answer[] { answer }, mgmtId, agentId); + } + + public Response(Request request, Answer[] answers) { + super(request, answers); + } + + public Response(Request request, Answer[] answers, long mgmtId, long agentId) { + super(request, answers); + _mgmtId = mgmtId; + _agentId = agentId; + } + + protected Response(Version ver, long seq, long agentId, long mgmtId, String ans, boolean inSequence, boolean stopOnError, boolean fromServer, boolean control) { + super(ver, seq, agentId, mgmtId, ans, inSequence, stopOnError, fromServer, control); + } + + public Answer getAnswer() { + Answer[] answers = getAnswers(); + return answers[0]; + } + + public Answer[] getAnswers() { + if (_cmds == null) { + final Gson json = s_gBuilder.create(); + _cmds = json.fromJson(_content, Answer[].class); + } + return (Answer[])_cmds; + } + + @Override + protected String getType() { + return "Ans: "; + } + + + public static Response parse(byte[] bytes) throws ClassNotFoundException, UnsupportedVersionException { + return (Response)Request.parse(bytes); + } +} diff --git a/core/src/com/cloud/agent/transport/UpgradeResponse.java b/core/src/com/cloud/agent/transport/UpgradeResponse.java new file mode 100644 index 00000000000..5930b0d5d0d --- /dev/null +++ b/core/src/com/cloud/agent/transport/UpgradeResponse.java @@ -0,0 +1,98 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.transport; + +import java.nio.ByteBuffer; + +import com.cloud.agent.api.Answer; +import com.cloud.utils.NumbersUtil; + +/** + * An UpgradeResponse is sent when there is a protocol version mismatched. + * It has the same header as the request but contains an url to the + * updated agent. + */ +public class UpgradeResponse extends Response { + byte[] _requestBytes; + + public UpgradeResponse(Request request, String url) { + super(request, new Answer[0]); + _requestBytes = null; + } + + public UpgradeResponse(byte[] request, String url) { + super(Version.v2, -1, -1, -1, url, true, true, true, false); + _requestBytes = request; + } + + protected UpgradeResponse(Version ver, long seq, String url) { + super(ver, seq, -1, -1, url, true, false, true, false); + _requestBytes = null; + } + + @Override + protected ByteBuffer serializeHeader(int contentSize) { + if (_requestBytes == null) { + return super.serializeHeader(contentSize); + } + + byte[] responseHeader = new byte[16]; + ByteBuffer buffer = ByteBuffer.wrap(responseHeader); + + buffer.put(_requestBytes[0]); // version number + buffer.put((byte)0); + buffer.putShort(getFlags()); + buffer.put(_requestBytes, 4, 8); // sequence number + buffer.putInt(contentSize); + buffer.flip(); + + return buffer; + } + + @Override + public String toString() { + if (_requestBytes == null) { + return super.toString(); + } + + final StringBuilder buffer = new StringBuilder(); + buffer.append("{ ").append(getType()); + buffer.append(", Seq: ").append(NumbersUtil.bytesToLong(_requestBytes, 4)).append(", Ver: ").append(_requestBytes[0]).append(", Flags: ").append(Integer.toBinaryString(getFlags())); + buffer.append(", ").append(_content).append(" }"); + return buffer.toString(); + } + + @Override + public ByteBuffer[] toBytes() { + ByteBuffer[] buffers = new ByteBuffer[2]; + + buffers[1] = ByteBuffer.wrap(_content.getBytes()); + buffers[0] = serializeHeader(buffers[1].capacity()); + + return buffers; + } + + public String getUpgradeUrl() { + return _content; + } + + @Override + protected short getFlags() { + return FLAG_RESPONSE | FLAG_UPDATE; + } +} diff --git a/core/src/com/cloud/agent/transport/VolListTypeAdaptor.java b/core/src/com/cloud/agent/transport/VolListTypeAdaptor.java new file mode 100644 index 00000000000..8615a5c3935 --- /dev/null +++ b/core/src/com/cloud/agent/transport/VolListTypeAdaptor.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.agent.transport; + +import java.lang.reflect.Type; +import java.util.List; + +import com.cloud.storage.VolumeVO; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.reflect.TypeToken; + +public class VolListTypeAdaptor implements JsonDeserializer>, JsonSerializer> { + static final GsonBuilder s_gBuilder; + static { + s_gBuilder = new GsonBuilder().excludeFieldsWithoutExposeAnnotation(); + } + + Type listType = new TypeToken>() {}.getType(); + + public VolListTypeAdaptor() { + } + + public JsonElement serialize(List src, Type typeOfSrc, JsonSerializationContext context) { + Gson json = s_gBuilder.create(); + String result = json.toJson(src, listType); + return new JsonPrimitive(result); + } + + public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + String jsonString = json.getAsJsonPrimitive().getAsString(); + Gson jsonp = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + List vols = jsonp.fromJson(jsonString, listType); + return vols; + } + +} \ No newline at end of file diff --git a/core/src/com/cloud/alert/AlertAdapter.java b/core/src/com/cloud/alert/AlertAdapter.java new file mode 100644 index 00000000000..576a5da8f43 --- /dev/null +++ b/core/src/com/cloud/alert/AlertAdapter.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.alert; + +import com.cloud.utils.component.Adapter; + +public interface AlertAdapter extends Adapter { +} diff --git a/core/src/com/cloud/alert/AlertManager.java b/core/src/com/cloud/alert/AlertManager.java new file mode 100644 index 00000000000..e2aee90d1d4 --- /dev/null +++ b/core/src/com/cloud/alert/AlertManager.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.alert; + +import com.cloud.capacity.CapacityVO; +import com.cloud.utils.component.Manager; + +public interface AlertManager extends Manager { + public static final short ALERT_TYPE_MEMORY = CapacityVO.CAPACITY_TYPE_MEMORY; + public static final short ALERT_TYPE_CPU = CapacityVO.CAPACITY_TYPE_CPU; + public static final short ALERT_TYPE_STORAGE = CapacityVO.CAPACITY_TYPE_STORAGE; + public static final short ALERT_TYPE_STORAGE_ALLOCATED = CapacityVO.CAPACITY_TYPE_STORAGE_ALLOCATED; + public static final short ALERT_TYPE_PUBLIC_IP = CapacityVO.CAPACITY_TYPE_PUBLIC_IP; + public static final short ALERT_TYPE_PRIVATE_IP = CapacityVO.CAPACITY_TYPE_PRIVATE_IP; + public static final short ALERT_TYPE_HOST = 6; + public static final short ALERT_TYPE_USERVM = 7; + public static final short ALERT_TYPE_DOMAIN_ROUTER = 8; + public static final short ALERT_TYPE_CONSOLE_PROXY = 9; + public static final short ALERT_TYPE_ROUTING = 10; // lost connection to default route (to the gateway) + public static final short ALERT_TYPE_STORAGE_MISC = 11; // lost connection to default route (to the gateway) + public static final short ALERT_TYPE_USAGE_SERVER = 12; // lost connection to default route (to the gateway) + public static final short ALERT_TYPE_MANAGMENT_NODE = 13; // lost connection to default route (to the gateway) + public static final short ALERT_TYPE_DOMAIN_ROUTER_MIGRATE = 14; + public static final short ALERT_TYPE_CONSOLE_PROXY_MIGRATE = 15; + public static final short ALERT_TYPE_USERVM_MIGRATE = 16; + public static final short ALERT_TYPE_VLAN = 17; + public static final short ALERT_TYPE_SSVM = 18; + + void clearAlert(short alertType, long dataCenterId, long podId); + void sendAlert(short alertType, long dataCenterId, Long podId, String subject, String body); + void recalculateCapacity(); +} diff --git a/core/src/com/cloud/alert/AlertVO.java b/core/src/com/cloud/alert/AlertVO.java new file mode 100644 index 00000000000..409bed65508 --- /dev/null +++ b/core/src/com/cloud/alert/AlertVO.java @@ -0,0 +1,144 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.alert; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="alert") +public class AlertVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="type") + private short type; + + @Column(name="pod_id") + private Long podId = null; + + @Column(name="data_center_id") + private long dataCenterId = 0; + + @Column(name="subject") + private String subject; + + @Column(name="sent_count") + private int sentCount = 0; + + @Column(name=GenericDao.CREATED_COLUMN) + private Date createdDate; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name="last_sent", updatable=true, nullable=true) + private Date lastSent; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name="resolved", updatable=true, nullable=true) + private Date resolved; + + public AlertVO() {} + public AlertVO(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public short getType() { + return type; + } + + public void setType(short type) { + this.type = type; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public Long getPodId() { + return podId; + } + + public void setPodId(Long podId) { + this.podId = podId; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public void setDataCenterId(long dataCenterId) { + this.dataCenterId = dataCenterId; + } + + public int getSentCount() { + return sentCount; + } + + public void setSentCount(int sentCount) { + this.sentCount = sentCount; + } + + public Date getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Date createdDate) { + this.createdDate = createdDate; + } + + public Date getLastSent() { + return lastSent; + } + + public void setLastSent(Date lastSent) { + this.lastSent = lastSent; + } + + public Date getResolved() { + return resolved; + } + + public void setResolved(Date resolved) { + this.resolved = resolved; + } +} diff --git a/core/src/com/cloud/alert/dao/AlertDao.java b/core/src/com/cloud/alert/dao/AlertDao.java new file mode 100644 index 00000000000..37da36ff903 --- /dev/null +++ b/core/src/com/cloud/alert/dao/AlertDao.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.alert.dao; + +import com.cloud.alert.AlertVO; +import com.cloud.utils.db.GenericDao; + +public interface AlertDao extends GenericDao { + AlertVO getLastAlert(short type, long dataCenterId, Long podId); +} diff --git a/core/src/com/cloud/alert/dao/AlertDaoImpl.java b/core/src/com/cloud/alert/dao/AlertDaoImpl.java new file mode 100644 index 00000000000..c79c6c48b9c --- /dev/null +++ b/core/src/com/cloud/alert/dao/AlertDaoImpl.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.alert.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.alert.AlertVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchCriteria; + +@Local(value = { AlertDao.class }) +public class AlertDaoImpl extends GenericDaoBase implements AlertDao { + @Override + public AlertVO getLastAlert(short type, long dataCenterId, Long podId) { + Filter searchFilter = new Filter(AlertVO.class, "createdDate", Boolean.FALSE, Long.valueOf(1), Long.valueOf(1)); + SearchCriteria sc = createSearchCriteria(); + + sc.addAnd("type", SearchCriteria.Op.EQ, Short.valueOf(type)); + sc.addAnd("dataCenterId", SearchCriteria.Op.EQ, Long.valueOf(dataCenterId)); + if (podId != null) { + sc.addAnd("podId", SearchCriteria.Op.EQ, podId); + } + + List alerts = listActiveBy(sc, searchFilter); + if ((alerts != null) && !alerts.isEmpty()) { + return alerts.get(0); + } + return null; + } +} diff --git a/core/src/com/cloud/async/AsyncInstanceCreateStatus.java b/core/src/com/cloud/async/AsyncInstanceCreateStatus.java new file mode 100644 index 00000000000..f2a8bf31937 --- /dev/null +++ b/core/src/com/cloud/async/AsyncInstanceCreateStatus.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async; + +public enum AsyncInstanceCreateStatus { + Creating, + Created, + Corrupted, + Failed; +} diff --git a/core/src/com/cloud/async/AsyncJobResult.java b/core/src/com/cloud/async/AsyncJobResult.java new file mode 100644 index 00000000000..43035ab10ac --- /dev/null +++ b/core/src/com/cloud/async/AsyncJobResult.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async; + +import com.cloud.serializer.SerializerHelper; + +public class AsyncJobResult { + public static final int STATUS_IN_PROGRESS = 0; + public static final int STATUS_SUCCEEDED = 1; + public static final int STATUS_FAILED = 2; + + private String cmdOriginator; + private long jobId; + private int jobStatus; + private int processStatus; + private int resultCode; + private String result; + + public AsyncJobResult(long jobId) { + this.jobId = jobId; + jobStatus = STATUS_IN_PROGRESS; + processStatus = 0; + resultCode = 0; + result = ""; + } + + public String getCmdOriginator() { + return cmdOriginator; + } + + public void setCmdOriginator(String cmdOriginator) { + this.cmdOriginator = cmdOriginator; + } + + public long getJobId() { + return jobId; + } + + public void setJobId(long jobId) { + this.jobId = jobId; + } + + public int getJobStatus() { + return jobStatus; + } + + public void setJobStatus(int jobStatus) { + this.jobStatus = jobStatus; + } + + public int getProcessStatus() { + return processStatus; + } + + public void setProcessStatus(int processStatus) { + this.processStatus = processStatus; + } + + public int getResultCode() { + return resultCode; + } + + public void setResultCode(int resultCode) { + this.resultCode = resultCode; + } + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + + public Object getResultObject() { + return SerializerHelper.fromSerializedString(result); + } + + public void setResultObject(Object result) { + this.result = SerializerHelper.toSerializedString(result); + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("AsyncJobResult {jobId:").append(getJobId()); + sb.append(", jobStatus: ").append(getJobStatus()); + sb.append(", processStatus: ").append(getProcessStatus()); + sb.append(", resultCode: ").append(getResultCode()); + sb.append(", result: ").append(result); + sb.append("}"); + return sb.toString(); + } +} diff --git a/core/src/com/cloud/async/AsyncJobVO.java b/core/src/com/cloud/async/AsyncJobVO.java new file mode 100644 index 00000000000..0e5fed8bf0a --- /dev/null +++ b/core/src/com/cloud/async/AsyncJobVO.java @@ -0,0 +1,331 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="async_job") +public class AsyncJobVO { + public static final int CALLBACK_POLLING = 0; + public static final int CALLBACK_EMAIL = 1; + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="user_id") + private long userId; + + @Column(name="account_id") + private long accountId; + + @Column(name="session_key") + private String sessionKey; + + @Column(name="job_cmd") + private String cmd; + + @Column(name="job_cmd_originator") + private String cmdOriginator; + + @Column(name="job_cmd_ver") + private int cmdVersion; + + @Column(name="job_cmd_info", length=65535) + private String cmdInfo; + + @Column(name="callback_type") + private int callbackType; + + @Column(name="callback_address") + private String callbackAddress; + + @Column(name="job_status") + private int status; + + @Column(name="job_process_status") + private int processStatus; + + @Column(name="job_result_code") + private int resultCode; + + @Column(name="job_result", length=65535) + private String result; + + @Column(name="instance_type", length=64) + private String instanceType; + + @Column(name="instance_id", length=64) + private Long instanceId; + + @Column(name="job_init_msid") + private Long initMsid; + + @Column(name="job_complete_msid") + private Long completeMsid; + + @Column(name=GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name="last_updated") + @Temporal(TemporalType.TIMESTAMP) + private Date lastUpdated; + + @Column(name="last_polled") + @Temporal(TemporalType.TIMESTAMP) + private Date lastPolled; + + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + public AsyncJobVO() { + } + + public AsyncJobVO(long userId, long accountId, String cmd, String cmdInfo) { + this.userId = userId; + this.accountId = accountId; + this.cmd = cmd; + this.cmdInfo = cmdInfo; + callbackType = CALLBACK_POLLING; + } + + public AsyncJobVO(long userId, long accountId, String cmd, String cmdInfo, + int callbackType, String callbackAddress) { + + this(userId, accountId, cmd, cmdInfo); + this.callbackType = callbackType; + this.callbackAddress = callbackAddress; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public long getUserId() { + return userId; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + public long getAccountId() { + return accountId; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public int getCmdVersion() { + return cmdVersion; + } + + public void setCmdVersion(int version) { + cmdVersion = version; + } + + public String getCmdInfo() { + return cmdInfo; + } + + public void setCmdInfo(String cmdInfo) { + this.cmdInfo = cmdInfo; + } + + public int getCallbackType() { + return callbackType; + } + + public void setCallbackType(int callbackType) { + this.callbackType = callbackType; + } + + public String getCallbackAddress() { + return callbackAddress; + } + + public void setCallbackAddress(String callbackAddress) { + this.callbackAddress = callbackAddress; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getProcessStatus() { + return processStatus; + } + + public void setProcessStatus(int status) { + processStatus = status; + } + + public int getResultCode() { + return resultCode; + } + + public void setResultCode(int resultCode) { + this.resultCode = resultCode; + } + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + + public Long getInitMsid() { + return initMsid; + } + + public void setInitMsid(Long initMsid) { + this.initMsid = initMsid; + } + + public Long getCompleteMsid() { + return completeMsid; + } + + public void setCompleteMsid(Long completeMsid) { + this.completeMsid = completeMsid; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(Date lastUpdated) { + this.lastUpdated = lastUpdated; + } + + public Date getLastPolled() { + return lastPolled; + } + + public void setLastPolled(Date lastPolled) { + this.lastPolled = lastPolled; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public String getInstanceType() { + return instanceType; + } + + public void setInstanceType(String instanceType) { + this.instanceType = instanceType; + } + + public Long getInstanceId() { + return instanceId; + } + + public void setInstanceId(Long instanceId) { + this.instanceId = instanceId; + } + + public String getSessionKey() { + return sessionKey; + } + + public void setSessionKey(String sessionKey) { + this.sessionKey = sessionKey; + } + + public String getCmdOriginator() { + return cmdOriginator; + } + + public void setCmdOriginator(String cmdOriginator) { + this.cmdOriginator = cmdOriginator; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("AsyncJobVO {id:").append(getId()); + sb.append(", userId: ").append(getUserId()); + sb.append(", accountId: ").append(getAccountId()); + sb.append(", sessionKey: ").append(getSessionKey()); + sb.append(", instanceType: ").append(getInstanceType()); + sb.append(", instanceId: ").append(getInstanceId()); + sb.append(", cmd: ").append(getCmd()); + sb.append(", cmdOriginator: ").append(getCmdOriginator()); + sb.append(", cmdInfo: ").append(getCmdInfo()); + sb.append(", cmdVersion: ").append(getCmdVersion()); + sb.append(", callbackType: ").append(getCallbackType()); + sb.append(", callbackAddress: ").append(getCallbackAddress()); + sb.append(", status: ").append(getStatus()); + sb.append(", processStatus: ").append(getProcessStatus()); + sb.append(", resultCode: ").append(getResultCode()); + sb.append(", result: ").append(getResult()); + sb.append(", initMsid: ").append(getInitMsid()); + sb.append(", completeMsid: ").append(getCompleteMsid()); + sb.append(", lastUpdated: ").append(getLastUpdated()); + sb.append(", lastPolled: ").append(getLastPolled()); + sb.append(", created: ").append(getCreated()); + sb.append("}"); + return sb.toString(); + } +} diff --git a/core/src/com/cloud/async/SyncQueueItemVO.java b/core/src/com/cloud/async/SyncQueueItemVO.java new file mode 100644 index 00000000000..6b0ac7b27cb --- /dev/null +++ b/core/src/com/cloud/async/SyncQueueItemVO.java @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="sync_queue_item") +public class SyncQueueItemVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="queue_id") + private Long queueId; + + @Column(name="content_type") + private String contentType; + + @Column(name="content_id") + private Long contentId; + + @Column(name="queue_proc_msid") + private Long lastProcessMsid; + + @Column(name="queue_proc_number") + private Long lastProcessNumber; + + @Column(name="created") + private Date created; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getQueueId() { + return queueId; + } + + public void setQueueId(Long queueId) { + this.queueId = queueId; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public Long getContentId() { + return contentId; + } + + public void setContentId(Long contentId) { + this.contentId = contentId; + } + + public Long getLastProcessMsid() { + return lastProcessMsid; + } + + public void setLastProcessMsid(Long lastProcessMsid) { + this.lastProcessMsid = lastProcessMsid; + } + + public Long getLastProcessNumber() { + return lastProcessNumber; + } + + public void setLastProcessNumber(Long lastProcessNumber) { + this.lastProcessNumber = lastProcessNumber; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("SyncQueueItemVO {id:").append(getId()).append(", queueId: ").append(getQueueId()); + sb.append(", contentType: ").append(getContentType()); + sb.append(", contentId: ").append(getContentId()); + sb.append(", lastProcessMsid: ").append(getLastProcessMsid()); + sb.append(", lastprocessNumber: ").append(getLastProcessNumber()); + sb.append(", created: ").append(getCreated()); + sb.append("}"); + return sb.toString(); + } +} diff --git a/core/src/com/cloud/async/SyncQueueVO.java b/core/src/com/cloud/async/SyncQueueVO.java new file mode 100644 index 00000000000..3e6fe51b4ca --- /dev/null +++ b/core/src/com/cloud/async/SyncQueueVO.java @@ -0,0 +1,141 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="sync_queue") +public class SyncQueueVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="sync_objtype") + private String syncObjType; + + @Column(name="sync_objid") + private Long syncObjId; + + @Column(name="queue_proc_number") + private Long lastProcessNumber; + + @Column(name="queue_proc_time") + @Temporal(TemporalType.TIMESTAMP) + private Date lastProcessTime; + + @Column(name="queue_proc_msid") + private Long lastProcessMsid; + + @Column(name="created") + @Temporal(TemporalType.TIMESTAMP) + private Date created; + + @Column(name="last_updated") + @Temporal(TemporalType.TIMESTAMP) + private Date lastUpdated; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getSyncObjType() { + return syncObjType; + } + + public void setSyncObjType(String syncObjType) { + this.syncObjType = syncObjType; + } + + public Long getSyncObjId() { + return syncObjId; + } + + public void setSyncObjId(Long syncObjId) { + this.syncObjId = syncObjId; + } + + public Long getLastProcessNumber() { + return lastProcessNumber; + } + + public void setLastProcessNumber(Long number) { + lastProcessNumber = number; + } + + public Date getLastProcessTime() { + return lastProcessTime; + } + + public void setLastProcessTime(Date lastProcessTime) { + this.lastProcessTime = lastProcessTime; + } + + public Long getLastProcessMsid() { + return lastProcessMsid; + } + + public void setLastProcessMsid(Long lastProcessMsid) { + this.lastProcessMsid = lastProcessMsid; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(Date lastUpdated) { + this.lastUpdated = lastUpdated; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("SyncQueueVO {id:").append(getId()); + sb.append(", syncObjType: ").append(getSyncObjType()); + sb.append(", syncObjId: ").append(getSyncObjId()); + sb.append(", lastProcessMsid: ").append(getLastProcessMsid()); + sb.append(", lastProcessNumber: ").append(getLastProcessNumber()); + sb.append(", lastProcessTime: ").append(getLastProcessTime()); + sb.append(", lastUpdated: ").append(getLastUpdated()); + sb.append(", created: ").append(getCreated()); + sb.append("}"); + return sb.toString(); + } +} diff --git a/core/src/com/cloud/async/dao/AsyncJobDao.java b/core/src/com/cloud/async/dao/AsyncJobDao.java new file mode 100644 index 00000000000..192a6fa05b2 --- /dev/null +++ b/core/src/com/cloud/async/dao/AsyncJobDao.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.async.AsyncJobVO; +import com.cloud.utils.db.GenericDao; + +public interface AsyncJobDao extends GenericDao { + AsyncJobVO findInstancePendingAsyncJob(String instanceType, long instanceId); + List getExpiredJobs(Date cutTime, int limit); +} diff --git a/core/src/com/cloud/async/dao/AsyncJobDaoImpl.java b/core/src/com/cloud/async/dao/AsyncJobDaoImpl.java new file mode 100644 index 00000000000..24cacaa8cc3 --- /dev/null +++ b/core/src/com/cloud/async/dao/AsyncJobDaoImpl.java @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async.dao; + +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.async.AsyncJobResult; +import com.cloud.async.AsyncJobVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value = { AsyncJobDao.class }) +public class AsyncJobDaoImpl extends GenericDaoBase implements AsyncJobDao { + private static final Logger s_logger = Logger.getLogger(AsyncJobDaoImpl.class.getName()); + + private SearchBuilder pendingAsyncJobSearch; + private SearchBuilder expiringAsyncJobSearch; + + public AsyncJobDaoImpl() { + pendingAsyncJobSearch = createSearchBuilder(); + pendingAsyncJobSearch.and("instanceType", pendingAsyncJobSearch.entity().getInstanceType(), + SearchCriteria.Op.EQ); + pendingAsyncJobSearch.and("instanceId", pendingAsyncJobSearch.entity().getInstanceId(), + SearchCriteria.Op.EQ); + pendingAsyncJobSearch.and("status", pendingAsyncJobSearch.entity().getStatus(), + SearchCriteria.Op.EQ); + pendingAsyncJobSearch.done(); + + expiringAsyncJobSearch = createSearchBuilder(); + expiringAsyncJobSearch.and("created", expiringAsyncJobSearch.entity().getCreated(), + SearchCriteria.Op.LTEQ); + expiringAsyncJobSearch.done(); + } + + public AsyncJobVO findInstancePendingAsyncJob(String instanceType, long instanceId) { + SearchCriteria sc = pendingAsyncJobSearch.create(); + sc.setParameters("instanceType", instanceType); + sc.setParameters("instanceId", instanceId); + sc.setParameters("status", AsyncJobResult.STATUS_IN_PROGRESS); + + List l = listBy(sc); + if(l != null && l.size() > 0) { + if(l.size() > 1) { + s_logger.warn("Instance " + instanceType + "-" + instanceId + " has multiple pending async-job"); + } + + return l.get(0); + } + return null; + } + + public List getExpiredJobs(Date cutTime, int limit) { + SearchCriteria sc = expiringAsyncJobSearch.create(); + sc.setParameters("created", cutTime); + Filter filter = new Filter(AsyncJobVO.class, "created", true, 0L, (long)limit); + return listBy(sc, filter); + } +} diff --git a/core/src/com/cloud/async/dao/SyncQueueDao.java b/core/src/com/cloud/async/dao/SyncQueueDao.java new file mode 100644 index 00000000000..a12f9043a25 --- /dev/null +++ b/core/src/com/cloud/async/dao/SyncQueueDao.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async.dao; + +import com.cloud.async.SyncQueueVO; +import com.cloud.utils.db.GenericDao; + +public interface SyncQueueDao extends GenericDao{ + public void ensureQueue(String syncObjType, long syncObjId); + public SyncQueueVO find(String syncObjType, long syncObjId); +} diff --git a/core/src/com/cloud/async/dao/SyncQueueDaoImpl.java b/core/src/com/cloud/async/dao/SyncQueueDaoImpl.java new file mode 100644 index 00000000000..d009866e9e0 --- /dev/null +++ b/core/src/com/cloud/async/dao/SyncQueueDaoImpl.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async.dao; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Date; +import java.util.TimeZone; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.async.SyncQueueVO; +import com.cloud.utils.DateUtil; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value = { SyncQueueDao.class }) +public class SyncQueueDaoImpl extends GenericDaoBase implements SyncQueueDao { + private static final Logger s_logger = Logger.getLogger(SyncQueueDaoImpl.class.getName()); + + SearchBuilder TypeIdSearch = createSearchBuilder(); + + @Override + public void ensureQueue(String syncObjType, long syncObjId) { + Date dt = DateUtil.currentGMTTime(); + String sql = "INSERT IGNORE INTO sync_queue(sync_objtype, sync_objid, created, last_updated) values(?, ?, ?, ?)"; + + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setString(1, syncObjType); + pstmt.setLong(2, syncObjId); + pstmt.setString(3, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), dt)); + pstmt.setString(4, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), dt)); + pstmt.execute(); + } catch (SQLException e) { + s_logger.warn("Unable to create sync queue " + syncObjType + "-" + syncObjId + ":" + e.getMessage(), e); + } catch (Throwable e) { + s_logger.warn("Unable to create sync queue " + syncObjType + "-" + syncObjId + ":" + e.getMessage(), e); + } + } + + @Override + public SyncQueueVO find(String syncObjType, long syncObjId) { + SearchCriteria sc = TypeIdSearch.create(); + sc.setParameters("syncObjType", syncObjType); + sc.setParameters("syncObjId", syncObjId); + return findOneActiveBy(sc); + } + + protected SyncQueueDaoImpl() { + super(); + TypeIdSearch = createSearchBuilder(); + TypeIdSearch.and("syncObjType", TypeIdSearch.entity().getSyncObjType(), SearchCriteria.Op.EQ); + TypeIdSearch.and("syncObjId", TypeIdSearch.entity().getSyncObjId(), SearchCriteria.Op.EQ); + TypeIdSearch.done(); + } +} diff --git a/core/src/com/cloud/async/dao/SyncQueueItemDao.java b/core/src/com/cloud/async/dao/SyncQueueItemDao.java new file mode 100644 index 00000000000..7b863af6f29 --- /dev/null +++ b/core/src/com/cloud/async/dao/SyncQueueItemDao.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async.dao; + +import java.util.List; + +import com.cloud.async.SyncQueueItemVO; +import com.cloud.utils.db.GenericDao; + +public interface SyncQueueItemDao extends GenericDao { + public SyncQueueItemVO getNextQueueItem(long queueId); + public List getNextQueueItems(int maxItems); + public List getActiveQueueItems(Long msid); +} diff --git a/core/src/com/cloud/async/dao/SyncQueueItemDaoImpl.java b/core/src/com/cloud/async/dao/SyncQueueItemDaoImpl.java new file mode 100644 index 00000000000..a0b4df70074 --- /dev/null +++ b/core/src/com/cloud/async/dao/SyncQueueItemDaoImpl.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.TimeZone; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.async.SyncQueueItemVO; +import com.cloud.utils.DateUtil; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value = { SyncQueueItemDao.class }) +public class SyncQueueItemDaoImpl extends GenericDaoBase implements SyncQueueItemDao { + private static final Logger s_logger = Logger.getLogger(SyncQueueItemDaoImpl.class); + + @Override + public SyncQueueItemVO getNextQueueItem(long queueId) { + + SearchBuilder sb = createSearchBuilder(); + sb.and("queueId", sb.entity().getQueueId(), SearchCriteria.Op.EQ); + sb.and("lastProcessNumber", sb.entity().getLastProcessNumber(), SearchCriteria.Op.NULL); + sb.done(); + + SearchCriteria sc = sb.create(); + sc.setParameters("queueId", queueId); + + Filter filter = new Filter(SyncQueueItemVO.class, "created", true, 0L, 1L); + List l = listActiveBy(sc, filter); + if(l != null && l.size() > 0) + return l.get(0); + + return null; + } + + @Override + public List getNextQueueItems(int maxItems) { + List l = new ArrayList(); + + String sql = "SELECT i.id, i.queue_id, i.content_type, i.content_id, i.created " + + " FROM sync_queue AS q JOIN sync_queue_item AS i ON q.id = i.queue_id " + + " WHERE q.queue_proc_time IS NULL AND i.queue_proc_number IS NULL " + + " GROUP BY q.id " + + " ORDER BY i.id " + + " LIMIT 0, ?"; + + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setInt(1, maxItems); + ResultSet rs = pstmt.executeQuery(); + while(rs.next()) { + SyncQueueItemVO item = new SyncQueueItemVO(); + item.setId(rs.getLong(1)); + item.setQueueId(rs.getLong(2)); + item.setContentType(rs.getString(3)); + item.setContentId(rs.getLong(4)); + item.setCreated(DateUtil.parseDateString(TimeZone.getTimeZone("GMT"), rs.getString(5))); + l.add(item); + } + } catch (SQLException e) { + s_logger.error("Unexpected sql excetpion, ", e); + } catch (Throwable e) { + s_logger.error("Unexpected excetpion, ", e); + } + return l; + } + + @Override + public List getActiveQueueItems(Long msid) { + SearchBuilder sb = createSearchBuilder(); + sb.and("lastProcessMsid", sb.entity().getLastProcessMsid(), + SearchCriteria.Op.EQ); + sb.done(); + + SearchCriteria sc = sb.create(); + sc.setParameters("lastProcessMsid", msid); + + Filter filter = new Filter(SyncQueueItemVO.class, "created", true, 0L, 1L); + return listActiveBy(sc, filter); + } +} diff --git a/core/src/com/cloud/capacity/CapacityVO.java b/core/src/com/cloud/capacity/CapacityVO.java new file mode 100644 index 00000000000..26aed06ffa4 --- /dev/null +++ b/core/src/com/cloud/capacity/CapacityVO.java @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.capacity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="op_host_capacity") +public class CapacityVO { + public static final short CAPACITY_TYPE_MEMORY = 0; + public static final short CAPACITY_TYPE_CPU = 1; + public static final short CAPACITY_TYPE_STORAGE = 2; + public static final short CAPACITY_TYPE_STORAGE_ALLOCATED = 3; + public static final short CAPACITY_TYPE_PUBLIC_IP = 4; + public static final short CAPACITY_TYPE_PRIVATE_IP = 5; + public static final short CAPACITY_TYPE_SECONDARY_STORAGE = 6; + + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="host_id") + private Long hostOrPoolId; + + @Column(name="data_center_id") + private long dataCenterId; + + @Column(name="pod_id") + private Long podId; + + @Column(name="used_capacity") + private long usedCapacity; + + @Column(name="total_capacity") + private long totalCapacity; + + @Column(name="capacity_type") + private short capacityType; + + public CapacityVO() {} + + public CapacityVO(Long hostId, long dataCenterId, Long podId, long usedCapacity, long totalCapacity, short capacityType) { + this.hostOrPoolId = hostId; + this.dataCenterId = dataCenterId; + this.podId = podId; + this.usedCapacity = usedCapacity; + this.totalCapacity = totalCapacity; + this.capacityType = capacityType; + } + + public Long getId() { + return id; + } + + public Long getHostOrPoolId() { + return hostOrPoolId; + } + + public void setHostId(Long hostId) { + this.hostOrPoolId = hostId; + } + public long getDataCenterId() { + return dataCenterId; + } + public void setDataCenterId(long dataCenterId) { + this.dataCenterId = dataCenterId; + } + + public Long getPodId() { + return podId; + } + public void setPodId(long podId) { + this.podId = new Long(podId); + } + public long getUsedCapacity() { + return usedCapacity; + } + public void setUsedCapacity(long usedCapacity) { + this.usedCapacity = usedCapacity; + } + public long getTotalCapacity() { + return totalCapacity; + } + public void setTotalCapacity(long totalCapacity) { + this.totalCapacity = totalCapacity; + } + public short getCapacityType() { + return capacityType; + } + public void setCapacityType(short capacityType) { + this.capacityType = capacityType; + } +} diff --git a/core/src/com/cloud/capacity/dao/CapacityDao.java b/core/src/com/cloud/capacity/dao/CapacityDao.java new file mode 100644 index 00000000000..f86bd97056f --- /dev/null +++ b/core/src/com/cloud/capacity/dao/CapacityDao.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.capacity.dao; + +import com.cloud.capacity.CapacityVO; +import com.cloud.utils.db.GenericDao; + +public interface CapacityDao extends GenericDao { + void setUsedStorage(Long hostId, long totalUsed); + void clearNonStorageCapacities(); + void clearStorageCapacities(); +} diff --git a/core/src/com/cloud/capacity/dao/CapacityDaoImpl.java b/core/src/com/cloud/capacity/dao/CapacityDaoImpl.java new file mode 100644 index 00000000000..7151bff7ce9 --- /dev/null +++ b/core/src/com/cloud/capacity/dao/CapacityDaoImpl.java @@ -0,0 +1,112 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.capacity.dao; + +import java.sql.PreparedStatement; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.capacity.CapacityVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.Transaction; + +@Local(value = { CapacityDao.class }) +public class CapacityDaoImpl extends GenericDaoBase implements CapacityDao { + private static final Logger s_logger = Logger.getLogger(CapacityDaoImpl.class); + + private static final String ADD_ALLOCATED_SQL = "UPDATE `cloud`.`op_host_capacity` SET used_capacity = used_capacity + ? WHERE host_id = ? AND capacity_type = ?"; + private static final String SUBTRACT_ALLOCATED_SQL = "UPDATE `cloud`.`op_host_capacity` SET used_capacity = used_capacity - ? WHERE host_id = ? AND capacity_type = ?"; + private static final String SET_USED_STORAGE_SQL = "UPDATE `cloud`.`op_host_capacity` SET used_capacity = ? WHERE host_id = ? AND capacity_type = 2"; + private static final String CLEAR_STORAGE_CAPACITIES = "DELETE FROM `cloud`.`op_host_capacity` WHERE capacity_type=2 OR capacity_type=6"; //clear storage and secondary_storage capacities + private static final String CLEAR_NON_STORAGE_CAPACITIES = "DELETE FROM `cloud`.`op_host_capacity` WHERE capacity_type<>2 AND capacity_type <>6"; //clear non-storage and non-secondary_storage capacities + + public void updateAllocated(Long hostId, long allocatedAmount, short capacityType, boolean add) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + txn.start(); + String sql = null; + if (add) { + sql = ADD_ALLOCATED_SQL; + } else { + sql = SUBTRACT_ALLOCATED_SQL; + } + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, allocatedAmount); + pstmt.setLong(2, hostId); + pstmt.setShort(3, capacityType); + pstmt.executeUpdate(); // TODO: Make sure exactly 1 row was updated? + txn.commit(); + } catch (Exception e) { + txn.rollback(); + s_logger.warn("Exception updating capacity for host: " + hostId, e); + } + } + + public void setUsedStorage(Long hostId, long totalUsed) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + txn.start(); + String sql = SET_USED_STORAGE_SQL; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, totalUsed); + pstmt.setLong(2, hostId); + pstmt.executeUpdate(); // TODO: Make sure exactly 1 row was updated? + txn.commit(); + } catch (Exception e) { + txn.rollback(); + s_logger.warn("Exception setting used storage for host: " + hostId, e); + } + } + + @Override + public void clearNonStorageCapacities() { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + txn.start(); + String sql = CLEAR_NON_STORAGE_CAPACITIES; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.executeUpdate(); + txn.commit(); + } catch (Exception e) { + txn.rollback(); + s_logger.warn("Exception clearing non storage capacities", e); + } + } + + @Override + public void clearStorageCapacities() { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + txn.start(); + String sql = CLEAR_STORAGE_CAPACITIES; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.executeUpdate(); + txn.commit(); + } catch (Exception e) { + txn.rollback(); + s_logger.warn("Exception clearing storage capacities", e); + } + } +} diff --git a/core/src/com/cloud/cluster/ClusterManager.java b/core/src/com/cloud/cluster/ClusterManager.java new file mode 100644 index 00000000000..aa3315909f5 --- /dev/null +++ b/core/src/com/cloud/cluster/ClusterManager.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.cluster; + +import com.cloud.agent.Listener; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Status.Event; +import com.cloud.utils.component.Manager; + +public interface ClusterManager extends Manager { + public static final int DEFAULT_HEARTBEAT_INTERVAL = 1500; + public static final int DEFAULT_HEARTBEAT_THRESHOLD = 60000; + public static final String ALERT_SUBJECT = "cluster-alert"; + + public Answer[] execute(String strPeer, long agentId, Command [] cmds, boolean stopOnError); + public long executeAsync(String strPeer, long agentId, Command[] cmds, boolean stopOnError, Listener listener); + public boolean onAsyncResult(String executingPeer, long agentId, long seq, Answer[] answers); + public boolean forwardAnswer(String targetPeer, long agentId, long seq, Answer[] answers); + + public Answer[] sendToAgent(Long hostId, Command [] cmds, boolean stopOnError) throws AgentUnavailableException, OperationTimedoutException; + public long sendToAgent(Long hostId, Command[] cmds, boolean stopOnError, Listener listener) throws AgentUnavailableException; + public boolean executeAgentUserRequest(long agentId, Event event) throws AgentUnavailableException; + public Boolean propagateAgentEvent(long agentId, Event event) throws AgentUnavailableException; + + public int getHeartbeatThreshold(); + public long getId(); + public String getSelfPeerName(); + public String getSelfNodeIP(); + public String getPeerName(long agentHostId); + + public void registerListener(ClusterManagerListener listener); + public void unregisterListener(ClusterManagerListener listener); + public ManagementServerHostVO getPeer(String peerName); + + /** + * Broadcast the command to all of the management server nodes. + * @param agentId agent id this broadcast is regarding + * @param cmds commands to broadcast + */ + public void broadcast(long agentId, Command[] cmds); +} diff --git a/core/src/com/cloud/cluster/ClusterManagerListener.java b/core/src/com/cloud/cluster/ClusterManagerListener.java new file mode 100644 index 00000000000..eede79a91d5 --- /dev/null +++ b/core/src/com/cloud/cluster/ClusterManagerListener.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.cluster; + +import java.util.List; + +public interface ClusterManagerListener { + void onManagementNodeJoined(List nodeList, long selfNodeId); + void onManagementNodeLeft(List nodeList, long selfNodeId); +} diff --git a/core/src/com/cloud/cluster/ClusterNodeJoinEventArgs.java b/core/src/com/cloud/cluster/ClusterNodeJoinEventArgs.java new file mode 100644 index 00000000000..80aaba3254c --- /dev/null +++ b/core/src/com/cloud/cluster/ClusterNodeJoinEventArgs.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.cluster; + +import java.util.List; + +import com.cloud.utils.events.EventArgs; + +public class ClusterNodeJoinEventArgs extends EventArgs { + private static final long serialVersionUID = 6284545402661799476L; + + private List joinedNodes; + private Long self; + + public ClusterNodeJoinEventArgs(Long self, List joinedNodes) { + super(ClusterManager.ALERT_SUBJECT); + + this.self = self; + this.joinedNodes = joinedNodes; + } + + public List getJoinedNodes() { + return joinedNodes; + } + + public Long getSelf() { + return self; + } +} diff --git a/core/src/com/cloud/cluster/ClusterNodeLeftEventArgs.java b/core/src/com/cloud/cluster/ClusterNodeLeftEventArgs.java new file mode 100644 index 00000000000..e1e9656dc38 --- /dev/null +++ b/core/src/com/cloud/cluster/ClusterNodeLeftEventArgs.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.cluster; + +import java.util.List; + +import com.cloud.utils.events.EventArgs; + +public class ClusterNodeLeftEventArgs extends EventArgs { + private static final long serialVersionUID = 7236743316223611935L; + + private List leftNodes; + private Long self; + + public ClusterNodeLeftEventArgs(Long self, List leftNodes) { + super(ClusterManager.ALERT_SUBJECT); + + this.self = self; + this.leftNodes = leftNodes; + } + + public List getLeftNodes() { + return leftNodes; + } + + public Long getSelf() { + return self; + } +} + diff --git a/core/src/com/cloud/cluster/ClusterService.java b/core/src/com/cloud/cluster/ClusterService.java new file mode 100644 index 00000000000..c961f59bc67 --- /dev/null +++ b/core/src/com/cloud/cluster/ClusterService.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.cluster; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +public interface ClusterService extends Remote { + String execute(String callingPeer, long agentId, String gsonPackage, boolean stopOnError) throws RemoteException; + long executeAsync(String callingPeer, long agentId, String gsonPackage, boolean stopOnError) throws RemoteException; + boolean onAsyncResult(String executingPeer, long agentId, long seq, String gsonPackage) throws RemoteException; +} diff --git a/core/src/com/cloud/cluster/ManagementServerHostVO.java b/core/src/com/cloud/cluster/ManagementServerHostVO.java new file mode 100644 index 00000000000..a7ec96edb2e --- /dev/null +++ b/core/src/com/cloud/cluster/ManagementServerHostVO.java @@ -0,0 +1,149 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.cluster; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="mshost") +public class ManagementServerHostVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="msid", updatable=true, nullable=false) + private Long msid; + + @Column(name="name", updatable=true, nullable=true) + private String name; + + @Column(name="version", updatable=true, nullable=true) + private String version; + + @Column(name="service_ip", updatable=true, nullable=false) + private String serviceIP; + + @Column(name="service_port", updatable=true, nullable=false) + private int servicePort; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name="last_update", updatable=true, nullable=true) + private Date lastUpdateTime; + + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + @Column(name="alert_count", updatable=true, nullable=false) + private int alertCount; + + public ManagementServerHostVO() { + } + + public ManagementServerHostVO(long msid, String serviceIP, int servicePort, Date updateTime) { + this.msid = msid; + this.serviceIP = serviceIP; + this.servicePort = servicePort; + this.lastUpdateTime = updateTime; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getMsid() { + return msid; + } + + public void setMsid(Long msid) { + this.msid = msid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getServiceIP() { + return serviceIP; + } + + public void setServiceIP(String serviceIP) { + this.serviceIP = serviceIP; + } + + public int getServicePort() { + return servicePort; + } + + public void setServicePort(int servicePort) { + this.servicePort = servicePort; + } + + public Date getLastUpdateTime() { + return lastUpdateTime; + } + + public void setLastUpdateTime(Date lastUpdateTime) { + this.lastUpdateTime = lastUpdateTime; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removedTime) { + removed = removedTime; + } + + public int getAlertCount() { + return alertCount; + } + + public void setAlertCount(int count) { + alertCount = count; + } +} diff --git a/core/src/com/cloud/cluster/RemoteMethodConstants.java b/core/src/com/cloud/cluster/RemoteMethodConstants.java new file mode 100644 index 00000000000..08159c1e0d4 --- /dev/null +++ b/core/src/com/cloud/cluster/RemoteMethodConstants.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.cluster; + +public interface RemoteMethodConstants { + public static final int METHOD_UNKNOWN = 0; + public static final int METHOD_EXECUTE = 1; + public static final int METHOD_EXECUTE_ASYNC = 2; + public static final int METHOD_ASYNC_RESULT = 3; +} diff --git a/core/src/com/cloud/cluster/dao/ManagementServerHostDao.java b/core/src/com/cloud/cluster/dao/ManagementServerHostDao.java new file mode 100644 index 00000000000..5318489ca58 --- /dev/null +++ b/core/src/com/cloud/cluster/dao/ManagementServerHostDao.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.cluster.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.utils.db.GenericDao; + +public interface ManagementServerHostDao extends GenericDao { + ManagementServerHostVO findByMsid(long msid); + void update(long id, String name, String version, String serviceIP, int servicePort, Date lastUpdate); + void update(long id, Date lastUpdate); + + List getActiveList(Date cutTime); + void increaseAlertCount(long id); +} diff --git a/core/src/com/cloud/cluster/dao/ManagementServerHostDaoImpl.java b/core/src/com/cloud/cluster/dao/ManagementServerHostDaoImpl.java new file mode 100644 index 00000000000..8fb19e21942 --- /dev/null +++ b/core/src/com/cloud/cluster/dao/ManagementServerHostDaoImpl.java @@ -0,0 +1,129 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.cluster.dao; + +import java.sql.PreparedStatement; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.utils.DateUtil; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value={ManagementServerHostDao.class}) +public class ManagementServerHostDaoImpl extends GenericDaoBase implements ManagementServerHostDao { + private static final Logger s_logger = Logger.getLogger(ManagementServerHostDaoImpl.class); + + private final SearchBuilder MsIdSearch; + + public ManagementServerHostVO findByMsid(long msid) { + SearchCriteria sc = MsIdSearch.create(); + sc.setParameters("msid", msid); + + List l = listBy(sc); + if(l != null && l.size() > 0) + return l.get(0); + + return null; + } + + public void update(long id, String name, String version, String serviceIP, int servicePort, Date lastUpdate) { + // Can't get removed column to work with null value, switch to raw JDBC that I can control + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + txn.start(); + + pstmt = txn.prepareAutoCloseStatement("update mshost set name=?, version=?, service_ip=?, service_port=?, last_update=?, removed=null, alert_count=0 where id=?"); + pstmt.setString(1, name); + pstmt.setString(2, version); + pstmt.setString(3, serviceIP); + pstmt.setInt(4, servicePort); + pstmt.setString(5, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), lastUpdate)); + pstmt.setLong(6, id); + + pstmt.executeUpdate(); + txn.commit(); + } catch(Exception e) { + s_logger.warn("Unexpected exception, ", e); + txn.rollback(); + } + } + + public void update(long id, Date lastUpdate) { + // Can't get removed column to work with null value, switch to raw JDBC that I can control + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + txn.start(); + + pstmt = txn.prepareAutoCloseStatement("update mshost set last_update=?, removed=null, alert_count=0 where id=?"); + pstmt.setString(1, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), lastUpdate)); + pstmt.setLong(2, id); + + pstmt.executeUpdate(); + txn.commit(); + } catch(Exception e) { + s_logger.warn("Unexpected exception, ", e); + txn.rollback(); + } + } + + public List getActiveList(Date cutTime) { + SearchBuilder activeSearch = createSearchBuilder(); + activeSearch.and("lastUpdateTime", activeSearch.entity().getLastUpdateTime(), SearchCriteria.Op.GT); + activeSearch.and("removed", activeSearch.entity().getRemoved(), SearchCriteria.Op.NULL); + + SearchCriteria sc = activeSearch.create(); + sc.setParameters("lastUpdateTime", cutTime); + + return listBy(sc); + } + + public void increaseAlertCount(long id) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + txn.start(); + + pstmt = txn.prepareAutoCloseStatement("update mshost set alert_count=alert_count+1 where id=?"); + pstmt.setLong(1, id); + + pstmt.executeUpdate(); + txn.commit(); + } catch(Exception e) { + s_logger.warn("Unexpected exception, ", e); + txn.rollback(); + } + } + + protected ManagementServerHostDaoImpl() { + MsIdSearch = createSearchBuilder(); + MsIdSearch.and("msid", MsIdSearch.entity().getMsid(), SearchCriteria.Op.EQ); + MsIdSearch.done(); + } +} diff --git a/core/src/com/cloud/configuration/ConfigurationVO.java b/core/src/com/cloud/configuration/ConfigurationVO.java new file mode 100644 index 00000000000..3246845582c --- /dev/null +++ b/core/src/com/cloud/configuration/ConfigurationVO.java @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="configuration") +public class ConfigurationVO { + @Column(name="instance") + private String instance; + + @Column(name="component") + private String component; + + @Id + @Column(name="name") + private String name; + + @Column(name="value") + private String value; + + @Column(name="description") + private String description; + + @Column(name="category") + private String category; + + protected ConfigurationVO() {} + + public ConfigurationVO(String category, String instance, String component, String name, String value, String description) { + this.category = category; + this.instance = instance; + this.component = component; + this.name = name; + this.value = value; + this.description = description; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getInstance() { + return instance; + } + + public void setInstance(String instance) { + this.instance = instance; + } + + public String getComponent() { + return component; + } + + public void setComponent(String component) { + this.component = component; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/core/src/com/cloud/configuration/ResourceCount.java b/core/src/com/cloud/configuration/ResourceCount.java new file mode 100644 index 00000000000..b9efaf45859 --- /dev/null +++ b/core/src/com/cloud/configuration/ResourceCount.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration; + +public interface ResourceCount { + + public enum ResourceType { + user_vm, + public_ip, + volume, + snapshot, + template + } + + public Long getId(); + + public void setId(Long id); + + public ResourceType getType(); + + public void setType(ResourceType type); + + public Long getAccountId(); + + public void setAccountId(Long accountId); + + public Long getDomainId(); + + public void setDomainId(Long domainId); + + public long getCount(); + + public void setCount(long count); + +} diff --git a/core/src/com/cloud/configuration/ResourceCountVO.java b/core/src/com/cloud/configuration/ResourceCountVO.java new file mode 100644 index 00000000000..99cc29ac8a3 --- /dev/null +++ b/core/src/com/cloud/configuration/ResourceCountVO.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="resource_count") +public class ResourceCountVO implements ResourceCount { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="type") + @Enumerated(EnumType.STRING) + private ResourceCount.ResourceType type; + + @Column(name="account_id") + private Long accountId; + + @Column(name="domain_id") + private Long domainId; + + @Column(name="count") + private long count; + + public ResourceCountVO() {} + + public ResourceCountVO(Long accountId, Long domainId, ResourceCount.ResourceType type, long count) { + this.accountId = accountId; + this.domainId = domainId; + this.type = type; + this.count = count; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ResourceCount.ResourceType getType() { + return type; + } + + public void setType(ResourceCount.ResourceType type) { + this.type = type; + } + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getDomainId() { + return domainId; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public long getCount() { + return count; + } + + public void setCount(long count) { + this.count = count; + } +} diff --git a/core/src/com/cloud/configuration/ResourceLimit.java b/core/src/com/cloud/configuration/ResourceLimit.java new file mode 100644 index 00000000000..e3dfb5e32e0 --- /dev/null +++ b/core/src/com/cloud/configuration/ResourceLimit.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration; + +public interface ResourceLimit { + + public Long getId(); + + public void setId(Long id); + + public ResourceCount.ResourceType getType(); + + public void setType(ResourceCount.ResourceType type); + + public Long getDomainId(); + + public void setDomainId(Long domainId); + + public Long getAccountId(); + + public void setAccountId(Long accountId); + + public Long getMax(); + + public void setMax(Long max); + +} diff --git a/core/src/com/cloud/configuration/ResourceLimitVO.java b/core/src/com/cloud/configuration/ResourceLimitVO.java new file mode 100644 index 00000000000..aaa57ddf80a --- /dev/null +++ b/core/src/com/cloud/configuration/ResourceLimitVO.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="resource_limit") +public class ResourceLimitVO implements ResourceLimit { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="type") + @Enumerated(EnumType.STRING) + private ResourceCount.ResourceType type; + + @Column(name="domain_id") + private Long domainId; + + @Column(name="account_id") + private Long accountId; + + @Column(name="max") + private Long max; + + public ResourceLimitVO() {} + + public ResourceLimitVO(Long domainId, Long accountId, ResourceCount.ResourceType type, Long max) { + this.domainId = domainId; + this.accountId = accountId; + this.type = type; + this.max = max; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ResourceCount.ResourceType getType() { + return type; + } + + public void setType(ResourceCount.ResourceType type) { + this.type = type; + } + + public Long getDomainId() { + return domainId; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getMax() { + return max; + } + + public void setMax(Long max) { + this.max = max; + } +} diff --git a/core/src/com/cloud/configuration/dao/ConfigurationDao.java b/core/src/com/cloud/configuration/dao/ConfigurationDao.java new file mode 100644 index 00000000000..35461950bb2 --- /dev/null +++ b/core/src/com/cloud/configuration/dao/ConfigurationDao.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration.dao; + +import java.util.Map; + +import com.cloud.configuration.ConfigurationVO; +import com.cloud.utils.db.GenericDao; + +public interface ConfigurationDao extends GenericDao { + + /** + * Retrieves the configuration for the a certain instance. It merges + * the configuration for the DEFAULT instance with the instance specified + * and the parameters passed in. + * + * The priority order in case of name collision is + * 1. params passed in. + * 2. configuration for the instance. + * 3. configuration for the DEFAULT instance. + * + * @param component component to retrieve it for. + * @param params parameters from the components.xml which will override the database values. + * @return a consolidated look at the configuration parameters. + */ + public Map getConfiguration(String instance, Map params); + + public Map getConfiguration(Map params); + + public Map getConfiguration(); + + /** + * Updates a configuration value + * @param name the name of the configuration value to update + * @param value the new value + * @return true if success, false if failure + */ + public boolean update(String name, String value); + + /** + * Gets the value for the specified configuration name + * @return value + */ + public String getValue(String name); + + +} diff --git a/core/src/com/cloud/configuration/dao/ConfigurationDaoImpl.java b/core/src/com/cloud/configuration/dao/ConfigurationDaoImpl.java new file mode 100644 index 00000000000..54930bcb1dc --- /dev/null +++ b/core/src/com/cloud/configuration/dao/ConfigurationDaoImpl.java @@ -0,0 +1,144 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration.dao; + +import java.sql.PreparedStatement; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.configuration.ConfigurationVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value={ConfigurationDao.class}) +public class ConfigurationDaoImpl extends GenericDaoBase implements ConfigurationDao { + private static final Logger s_logger = Logger.getLogger(ConfigurationDaoImpl.class); + private Map _configs = null; + + final SearchBuilder InstanceSearch; + final SearchBuilder NameSearch; + + public static final String UPDATE_CONFIGURATION_SQL = "UPDATE configuration SET value = ? WHERE name = ?"; + public static final String GET_CONFIGURATION_VALUE_SQL = "SELECT * FROM configuration WHERE name = ?"; + + public ConfigurationDaoImpl () { + InstanceSearch = createSearchBuilder(); + InstanceSearch.and("instance", InstanceSearch.entity().getInstance(), SearchCriteria.Op.EQ); + + NameSearch = createSearchBuilder(); + NameSearch.and("name", NameSearch.entity().getName(), SearchCriteria.Op.EQ); + } + + @Override + public Map getConfiguration(String instance, Map params) { + if (_configs == null) { + _configs = new HashMap(); + + SearchCriteria sc = InstanceSearch.create(); + sc.setParameters("instance", "DEFAULT"); + + List configurations = listBy(sc); + + for (ConfigurationVO config : configurations) { + if (config.getValue() != null) + _configs.put(config.getName(), config.getValue()); + } + + sc = InstanceSearch.create(); + sc.setParameters("instance", instance); + + configurations = listBy(sc); + + for (ConfigurationVO config : configurations) { + if (config.getValue() != null) + _configs.put(config.getName(), config.getValue()); + } + + } + + mergeConfigs(_configs, params); + return _configs; + } + + @Override + public Map getConfiguration(Map params) { + return getConfiguration("DEFAULT", params); + } + + @Override + public Map getConfiguration() { + return getConfiguration("DEFAULT", new HashMap()); + } + + protected void mergeConfigs(Map dbParams, Map xmlParams) { + for (Map.Entry param : xmlParams.entrySet()) { + dbParams.put(param.getKey(), (String)param.getValue()); + } + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + + return true; + } + + @Override + public boolean update(String name, String value) { + Transaction txn = Transaction.currentTxn(); + try { + PreparedStatement stmt = txn.prepareStatement(UPDATE_CONFIGURATION_SQL); + stmt.setString(1, value); + stmt.setString(2, name); + stmt.executeUpdate(); + return true; + } catch (Exception e) { + s_logger.warn("Unable to update Configuration Value", e); + } + return false; + } + + @Override + public String getValue(String name) { + SearchCriteria sc = NameSearch.create(); + sc.setParameters("name", name); + List configurations = listBy(sc); + + if (configurations.size() == 0) { + return null; + } + + ConfigurationVO config = configurations.get(0); + String value = config.getValue(); + + if (value == null) { + return ""; + } else { + return value; + } + } +} diff --git a/core/src/com/cloud/configuration/dao/ResourceCountDao.java b/core/src/com/cloud/configuration/dao/ResourceCountDao.java new file mode 100644 index 00000000000..6925f5c08b2 --- /dev/null +++ b/core/src/com/cloud/configuration/dao/ResourceCountDao.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration.dao; + +import com.cloud.configuration.ResourceCountVO; +import com.cloud.configuration.ResourceCount.ResourceType; +import com.cloud.utils.db.GenericDao; + +public interface ResourceCountDao extends GenericDao { + + /** + * Get the count of in use resources for an account by type + * @param accountId the id of the account to get the resource count + * @param type the type of resource (e.g. user_vm, public_ip, volume) + * @return the count of resources in use for the given type and account + */ + public long getAccountCount(long accountId, ResourceType type); + + /** + * Get the count of in use resources for a domain by type + * @param domainId the id of the domain to get the resource count + * @param type the type of resource (e.g. user_vm, public_ip, volume) + * @return the count of resources in use for the given type and domain + */ + public long getDomainCount(long domainId, ResourceType type); + + /** + * Update the count of resources in use for the given account and given resource type + * @param accountId the id of the account to update resource count + * @param type the type of resource (e.g. user_vm, public_ip, volume) + * @param increment whether the change is adding or subtracting from the current count + * @param delta the number of resources being added/released + */ + public void updateAccountCount(long accountId, ResourceType type, boolean increment, long delta); + + /** + * Update the count of resources in use for the given domain and given resource type + * @param domainId the id of the domain to update resource count + * @param type the type of resource (e.g. user_vm, public_ip, volume) + * @param increment whether the change is adding or subtracting from the current count + * @param delta the number of resources being added/released + */ + public void updateDomainCount(long domainId, ResourceType type, boolean increment, long delta); +} diff --git a/core/src/com/cloud/configuration/dao/ResourceCountDaoImpl.java b/core/src/com/cloud/configuration/dao/ResourceCountDaoImpl.java new file mode 100644 index 00000000000..e7921449ff2 --- /dev/null +++ b/core/src/com/cloud/configuration/dao/ResourceCountDaoImpl.java @@ -0,0 +1,112 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration.dao; + +import javax.ejb.Local; + +import com.cloud.configuration.ResourceCount.ResourceType; +import com.cloud.configuration.ResourceCountVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={ResourceCountDao.class}) +public class ResourceCountDaoImpl extends GenericDaoBase implements ResourceCountDao { + private SearchBuilder IdTypeSearch; + private SearchBuilder DomainIdTypeSearch; + + public ResourceCountDaoImpl() { + IdTypeSearch = createSearchBuilder(); + IdTypeSearch.and("type", IdTypeSearch.entity().getType(), SearchCriteria.Op.EQ); + IdTypeSearch.and("accountId", IdTypeSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + IdTypeSearch.done(); + + DomainIdTypeSearch = createSearchBuilder(); + DomainIdTypeSearch.and("type", DomainIdTypeSearch.entity().getType(), SearchCriteria.Op.EQ); + DomainIdTypeSearch.and("domainId", DomainIdTypeSearch.entity().getDomainId(), SearchCriteria.Op.EQ); + DomainIdTypeSearch.done(); + } + + private ResourceCountVO findByAccountIdAndType(long accountId, ResourceType type) { + if (type == null) { + return null; + } + + SearchCriteria sc = IdTypeSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("type", type); + + return findOneBy(sc); + } + + private ResourceCountVO findByDomainIdAndType(long domainId, ResourceType type) { + if (type == null) { + return null; + } + + SearchCriteria sc = DomainIdTypeSearch.create(); + sc.setParameters("domainId", domainId); + sc.setParameters("type", type); + + return findOneBy(sc); + } + + @Override + public long getAccountCount(long accountId, ResourceType type) { + ResourceCountVO resourceCountVO = findByAccountIdAndType(accountId, type); + return (resourceCountVO != null) ? resourceCountVO.getCount() : 0; + } + + @Override + public long getDomainCount(long domainId, ResourceType type) { + ResourceCountVO resourceCountVO = findByDomainIdAndType(domainId, type); + return (resourceCountVO != null) ? resourceCountVO.getCount() : 0; + } + + @Override + public void updateAccountCount(long accountId, ResourceType type, boolean increment, long delta) { + delta = increment ? delta : delta * -1; + + ResourceCountVO resourceCountVO = findByAccountIdAndType(accountId, type); + + if (resourceCountVO == null) { + resourceCountVO = new ResourceCountVO(accountId, null, type, 0); + resourceCountVO.setCount(resourceCountVO.getCount() + delta); + persist(resourceCountVO); + } else { + resourceCountVO.setCount(resourceCountVO.getCount() + delta); + update(resourceCountVO.getId(), resourceCountVO); + } + } + + @Override + public void updateDomainCount(long domainId, ResourceType type, boolean increment, long delta) { + delta = increment ? delta : delta * -1; + + ResourceCountVO resourceCountVO = findByDomainIdAndType(domainId, type); + if (resourceCountVO == null) { + resourceCountVO = new ResourceCountVO(null, domainId, type, 0); + resourceCountVO.setCount(resourceCountVO.getCount() + delta); + persist(resourceCountVO); + } else { + resourceCountVO.setCount(resourceCountVO.getCount() + delta); + update(resourceCountVO.getId(), resourceCountVO); + } + } +} \ No newline at end of file diff --git a/core/src/com/cloud/configuration/dao/ResourceLimitDao.java b/core/src/com/cloud/configuration/dao/ResourceLimitDao.java new file mode 100644 index 00000000000..6f48d331c65 --- /dev/null +++ b/core/src/com/cloud/configuration/dao/ResourceLimitDao.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration.dao; + +import java.util.List; + +import com.cloud.configuration.ResourceCount; +import com.cloud.configuration.ResourceLimitVO; +import com.cloud.utils.db.GenericDao; + +public interface ResourceLimitDao extends GenericDao { + + public ResourceLimitVO findByDomainIdAndType(Long domainId, ResourceCount.ResourceType type); + public ResourceLimitVO findByAccountIdAndType(Long accountId, ResourceCount.ResourceType type); + public List listByAccountId(Long accountId); + public List listByDomainId(Long domainId); + public boolean update(Long id, Long max); + public ResourceCount.ResourceType getLimitType(String type); +} diff --git a/core/src/com/cloud/configuration/dao/ResourceLimitDaoImpl.java b/core/src/com/cloud/configuration/dao/ResourceLimitDaoImpl.java new file mode 100644 index 00000000000..d6b4a002f22 --- /dev/null +++ b/core/src/com/cloud/configuration/dao/ResourceLimitDaoImpl.java @@ -0,0 +1,105 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.configuration.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.configuration.ResourceCount; +import com.cloud.configuration.ResourceLimitVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={ResourceLimitDao.class}) +public class ResourceLimitDaoImpl extends GenericDaoBase implements ResourceLimitDao { + private SearchBuilder IdTypeSearch; + + public ResourceLimitDaoImpl () { + IdTypeSearch = createSearchBuilder(); + IdTypeSearch.and("type", IdTypeSearch.entity().getType(), SearchCriteria.Op.EQ); + IdTypeSearch.and("domainId", IdTypeSearch.entity().getDomainId(), SearchCriteria.Op.EQ); + IdTypeSearch.and("accountId", IdTypeSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + IdTypeSearch.done(); + } + + public ResourceLimitVO findByDomainIdAndType(Long domainId, ResourceCount.ResourceType type) { + if (domainId == null || type == null) + return null; + + SearchCriteria sc = IdTypeSearch.create(); + sc.setParameters("domainId", domainId); + sc.setParameters("type", type); + + return findOneBy(sc); + } + + public List listByDomainId(Long domainId) { + if (domainId == null) + return null; + + SearchCriteria sc = IdTypeSearch.create(); + sc.setParameters("domainId", domainId); + + return listBy(sc); + } + + public ResourceLimitVO findByAccountIdAndType(Long accountId, ResourceCount.ResourceType type) { + if (accountId == null || type == null) + return null; + + SearchCriteria sc = IdTypeSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("type", type); + + return findOneBy(sc); + } + + public List listByAccountId(Long accountId) { + if (accountId == null) + return null; + + SearchCriteria sc = IdTypeSearch.create(); + sc.setParameters("accountId", accountId); + + return listBy(sc); + } + + public boolean update(Long id, Long max) { + ResourceLimitVO limit = findById(id); + if (max != null) + limit.setMax(max); + else + limit.setMax(new Long(-1)); + return update(id, limit); + } + + public ResourceCount.ResourceType getLimitType(String type) { + ResourceCount.ResourceType[] validTypes = ResourceCount.ResourceType.values(); + + for (ResourceCount.ResourceType validType : validTypes) { + if (validType.toString().equals(type)) { + return validType; + } + } + + return null; + } +} diff --git a/core/src/com/cloud/consoleproxy/ConsoleProxyAlertEventArgs.java b/core/src/com/cloud/consoleproxy/ConsoleProxyAlertEventArgs.java new file mode 100644 index 00000000000..0ff44b205e7 --- /dev/null +++ b/core/src/com/cloud/consoleproxy/ConsoleProxyAlertEventArgs.java @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import com.cloud.utils.events.EventArgs; +import com.cloud.vm.ConsoleProxyVO; + +public class ConsoleProxyAlertEventArgs extends EventArgs { + + private static final long serialVersionUID = 23773987551479885L; + + public static final int PROXY_CREATED = 1; + public static final int PROXY_UP = 2; + public static final int PROXY_DOWN = 3; + public static final int PROXY_CREATE_FAILURE = 4; + public static final int PROXY_START_FAILURE = 5; + public static final int PROXY_FIREWALL_ALERT = 6; + public static final int PROXY_STORAGE_ALERT = 7; + public static final int PROXY_REBOOTED = 8; + + private int type; + private long zoneId; + private long proxyId; + private ConsoleProxyVO proxy; + private String message; + + public ConsoleProxyAlertEventArgs(int type, long zoneId, + long proxyId, ConsoleProxyVO proxy, String message) { + + super(ConsoleProxyManager.ALERT_SUBJECT); + this.type = type; + this.zoneId = zoneId; + this.proxyId = proxyId; + this.proxy = proxy; + this.message = message; + } + + public int getType() { + return type; + } + + public long getZoneId() { + return zoneId; + } + + public long getProxyId() { + return proxyId; + } + + public ConsoleProxyVO getProxy() { + return proxy; + } + + public String getMessage() { + return message; + } +} diff --git a/core/src/com/cloud/consoleproxy/ConsoleProxyAllocator.java b/core/src/com/cloud/consoleproxy/ConsoleProxyAllocator.java new file mode 100644 index 00000000000..18f86f4876e --- /dev/null +++ b/core/src/com/cloud/consoleproxy/ConsoleProxyAllocator.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import java.util.List; +import java.util.Map; + +import com.cloud.utils.component.Adapter; +import com.cloud.vm.ConsoleProxyVO; + +public interface ConsoleProxyAllocator extends Adapter { + public ConsoleProxyVO allocProxy(List candidates, Map loadInfo, long dataCenterId); +} diff --git a/core/src/com/cloud/consoleproxy/ConsoleProxyListener.java b/core/src/com/cloud/consoleproxy/ConsoleProxyListener.java new file mode 100644 index 00000000000..e0aa2e5750a --- /dev/null +++ b/core/src/com/cloud/consoleproxy/ConsoleProxyListener.java @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.consoleproxy; + + +import org.apache.log4j.Logger; + +import com.cloud.agent.Listener; +import com.cloud.agent.api.AgentControlAnswer; +import com.cloud.agent.api.AgentControlCommand; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.ConsoleAccessAuthenticationCommand; +import com.cloud.agent.api.ConsoleProxyLoadReportCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.host.HostVO; +import com.cloud.host.Status; + +public class ConsoleProxyListener implements Listener { + private final static Logger s_logger = Logger.getLogger(ConsoleProxyListener.class); + + ConsoleProxyManager _proxyMgr = null; + + public ConsoleProxyListener(ConsoleProxyManager proxyMgr) { + _proxyMgr = proxyMgr; + } + + @Override + public boolean isRecurring() { + return true; + } + + @Override + public boolean processAnswer(long agentId, long seq, Answer[] answers) { + return true; + } + + @Override + public boolean processCommand(long agentId, long seq, Command[] commands) { + return false; + } + + @Override + public AgentControlAnswer processControlCommand(long agentId, AgentControlCommand cmd) { + if(cmd instanceof ConsoleProxyLoadReportCommand) { + _proxyMgr.onLoadReport((ConsoleProxyLoadReportCommand)cmd); + + // return dummy answer + return new AgentControlAnswer(cmd); + } else if(cmd instanceof ConsoleAccessAuthenticationCommand) { + return _proxyMgr.onConsoleAccessAuthentication((ConsoleAccessAuthenticationCommand)cmd); + } + return null; + } + + @Override + public boolean processConnect(HostVO host, StartupCommand cmd) { + _proxyMgr.onAgentConnect(host, cmd); + return true; + } + + @Override + public boolean processDisconnect(long agentId, Status state) { + _proxyMgr.onAgentDisconnect(agentId, state); + return true; + } + + @Override + public boolean processTimeout(long agentId, long seq) { + return true; + } + + @Override + public int getTimeout() { + return -1; + } +} diff --git a/core/src/com/cloud/consoleproxy/ConsoleProxyManager.java b/core/src/com/cloud/consoleproxy/ConsoleProxyManager.java new file mode 100644 index 00000000000..ca36c338f82 --- /dev/null +++ b/core/src/com/cloud/consoleproxy/ConsoleProxyManager.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.consoleproxy; + +import com.cloud.agent.api.AgentControlAnswer; +import com.cloud.agent.api.ConsoleAccessAuthenticationCommand; +import com.cloud.agent.api.ConsoleProxyLoadReportCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.utils.component.Manager; +import com.cloud.vm.ConsoleProxyVO; + +public interface ConsoleProxyManager extends Manager { + public static final int DEFAULT_PROXY_CAPACITY = 50; + public static final int DEFAULT_STANDBY_CAPACITY = 10; + public static final int DEFAULT_PROXY_VM_RAMSIZE = 1024; // 1G + + public static final int DEFAULT_PROXY_CMD_PORT = 8001; + public static final int DEFAULT_PROXY_VNC_PORT = 0; + public static final int DEFAULT_PROXY_URL_PORT = 80; + public static final int DEFAULT_PROXY_SESSION_TIMEOUT = 300000; // 5 minutes + + public static final String ALERT_SUBJECT = "proxy-alert"; + + public ConsoleProxyVO assignProxy(long dataCenterId, long userVmId); + + public ConsoleProxyVO startProxy(long proxyVmId, long startEventId); + public boolean stopProxy(long proxyVmId, long startEventId); + public boolean rebootProxy(long proxyVmId, long startEventId); + public boolean destroyProxy(long proxyVmId, long startEventId); + + public void onLoadReport(ConsoleProxyLoadReportCommand cmd); + public AgentControlAnswer onConsoleAccessAuthentication(ConsoleAccessAuthenticationCommand cmd); + + public void onAgentConnect(HostVO host, StartupCommand cmd); + public void onAgentDisconnect(long agentId, Status state); +} diff --git a/core/src/com/cloud/dc/AccountVlanMapVO.java b/core/src/com/cloud/dc/AccountVlanMapVO.java new file mode 100644 index 00000000000..a5fbb105e1d --- /dev/null +++ b/core/src/com/cloud/dc/AccountVlanMapVO.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="account_vlan_map") +public class AccountVlanMapVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="account_id") + private long accountId; + + @Column(name="vlan_db_id") + private long vlanDbId; + + public AccountVlanMapVO(long accountId, long vlanDbId) { + this.accountId = accountId; + this.vlanDbId = vlanDbId; + } + + public AccountVlanMapVO() { + + } + + public Long getId() { + return id; + } + + public long getAccountId() { + return accountId; + } + + public long getVlanDbId() { + return vlanDbId; + } +} diff --git a/core/src/com/cloud/dc/ClusterVO.java b/core/src/com/cloud/dc/ClusterVO.java new file mode 100644 index 00000000000..acb10a023e2 --- /dev/null +++ b/core/src/com/cloud/dc/ClusterVO.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.utils.NumbersUtil; + +@Entity +@Table(name="cluster") +public class ClusterVO { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="id") + long id; + + @Column(name="name") + String name; + + @Column(name="data_center_id") + long dataCenterId; + + @Column(name="pod_id") + long podId; + + public ClusterVO() { + } + + public ClusterVO(long dataCenterId, long podId, String name) { + this.dataCenterId = dataCenterId; + this.podId = podId; + this.name = name; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public long getPodId() { + return podId; + } + + public ClusterVO(long clusterId) { + this.id = clusterId; + } + + @Override + public int hashCode() { + return NumbersUtil.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ClusterVO)) { + return false; + } + ClusterVO that = (ClusterVO)obj; + return this.id == that.id; + } +} diff --git a/core/src/com/cloud/dc/DataCenterIpAddressVO.java b/core/src/com/cloud/dc/DataCenterIpAddressVO.java new file mode 100755 index 00000000000..52a3b591e05 --- /dev/null +++ b/core/src/com/cloud/dc/DataCenterIpAddressVO.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="op_dc_ip_address_alloc") +public class DataCenterIpAddressVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + long id; + + @Column(name="ip_address", updatable=false, nullable=false) + String ipAddress; + + @Column(name="taken") + @Temporal(value=TemporalType.TIMESTAMP) + private Date takenAt; + + @Column(name="data_center_id", updatable=false, nullable=false) + private long dataCenterId; + + @Column(name="pod_id", updatable=false, nullable=false) + private long podId; + + @Column(name="instance_id") + private Long instanceId; + + protected DataCenterIpAddressVO() { + } + + public DataCenterIpAddressVO(String ipAddress, long dataCenterId, long podId) { + this.ipAddress = ipAddress; + this.dataCenterId = dataCenterId; + this.podId = podId; + } + + public Long getId() { + return id; + } + + public Long getInstanceId() { + return instanceId; + } + + public void setInstanceId(Long instanceId) { + this.instanceId = instanceId; + } + + public long getPodId() { + return podId; + } + + public void setTakenAt(Date takenDate) { + this.takenAt = takenDate; + } + + public String getIpAddress() { + return ipAddress; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public Date getTakenAt() { + return takenAt; + } +} diff --git a/core/src/com/cloud/dc/DataCenterLinkLocalIpAddressVO.java b/core/src/com/cloud/dc/DataCenterLinkLocalIpAddressVO.java new file mode 100644 index 00000000000..306637497b9 --- /dev/null +++ b/core/src/com/cloud/dc/DataCenterLinkLocalIpAddressVO.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="op_dc_link_local_ip_address_alloc") +public class DataCenterLinkLocalIpAddressVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + long id; + + @Column(name="ip_address", updatable=false, nullable=false) + String ipAddress; + + @Column(name="taken") + @Temporal(value=TemporalType.TIMESTAMP) + private Date takenAt; + + @Column(name="data_center_id", updatable=false, nullable=false) + private long dataCenterId; + + @Column(name="pod_id", updatable=false, nullable=false) + private long podId; + + @Column(name="instance_id") + private Long instanceId; + + protected DataCenterLinkLocalIpAddressVO() { + } + + public DataCenterLinkLocalIpAddressVO(String ipAddress, long dataCenterId, long podId) { + this.ipAddress = ipAddress; + this.dataCenterId = dataCenterId; + this.podId = podId; + } + + public Long getId() { + return id; + } + + public Long getInstanceId() { + return instanceId; + } + + public void setInstanceId(Long instanceId) { + this.instanceId = instanceId; + } + + public long getPodId() { + return podId; + } + + public void setTakenAt(Date takenDate) { + this.takenAt = takenDate; + } + + public String getIpAddress() { + return ipAddress; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public Date getTakenAt() { + return takenAt; + } +} diff --git a/core/src/com/cloud/dc/DataCenterVO.java b/core/src/com/cloud/dc/DataCenterVO.java new file mode 100644 index 00000000000..c61c335ec8c --- /dev/null +++ b/core/src/com/cloud/dc/DataCenterVO.java @@ -0,0 +1,162 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.TableGenerator; + +@Entity +@Table(name="data_center") +public class DataCenterVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="name") + private String name = null; + + @Column(name="description") + private String description = null; + + @Column(name="dns1") + private String dns1 = null; + + @Column(name="dns2") + private String dns2 = null; + + @Column(name="internal_dns1") + private String internalDns1 = null; + + @Column(name="internal_dns2") + private String internalDns2 = null; + + @Column(name="router_mac_address", updatable = false, nullable=false) + private String routerMacAddress = "02:00:00:00:00:01"; + + @Column(name="vnet") + private String vnet = null; + + @Column(name="guest_network_cidr") + private String guestNetworkCidr = null; + + @Column(name="mac_address", updatable = false, nullable=false) + @TableGenerator(name="mac_address_sq", table="data_center", pkColumnName="id", valueColumnName="mac_address", allocationSize=1) + private long macAddress = 1; + + public DataCenterVO(Long id, String name, String description, String dns1, String dns2, String dns3, String dns4, String vnet, String guestCidr) { + this.id = id; + this.name = name; + this.description = description; + this.dns1 = dns1; + this.dns2 = dns2; + this.internalDns1 = dns3; + this.internalDns2 = dns4; + this.vnet = vnet; + this.guestNetworkCidr = guestCidr; + } + + public DataCenterVO(String name, String description, String dns1, String dns2, String dns3, String dns4, String vnet, String guestCidr) { + this(null, name, description, dns1, dns2, dns3, dns4, vnet, guestCidr); + } + + public String getDescription() { + return description; + } + + public String getRouterMacAddress() { + return routerMacAddress; + } + + public void setVnet(String vnet) { + this.vnet = vnet; + } + + public String getDns1() { + return dns1; + } + + public String getVnet() { + return vnet; + } + + public String getDns2() { + return dns2; + } + + public String getInternalDns1() { + return internalDns1; + } + + public String getInternalDns2() { + return internalDns2; + } + + protected DataCenterVO() { + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void setDns1(String dns1) { + this.dns1 = dns1; + } + + public void setDns2(String dns2) { + this.dns2 = dns2; + } + + public void setInternalDns1(String dns3) { + this.internalDns1 = dns3; + } + + public void setInternalDns2(String dns4) { + this.internalDns2 = dns4; + } + + public void setRouterMacAddress(String routerMacAddress) { + this.routerMacAddress = routerMacAddress; + } + + public String getGuestNetworkCidr() + { + return guestNetworkCidr; + } + + public void setGuestNetworkCidr(String guestNetworkCidr) + { + this.guestNetworkCidr = guestNetworkCidr; + } + +} diff --git a/core/src/com/cloud/dc/DataCenterVnetVO.java b/core/src/com/cloud/dc/DataCenterVnetVO.java new file mode 100755 index 00000000000..40da1097200 --- /dev/null +++ b/core/src/com/cloud/dc/DataCenterVnetVO.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="op_dc_vnet_alloc") +public class DataCenterVnetVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + Long id; + + @Column(name="taken", nullable=true) + @Temporal(value=TemporalType.TIMESTAMP) + Date takenAt; + + @Column(name="vnet", updatable=false, nullable=false) + protected String vnet; + + @Column(name="data_center_id", updatable=false, nullable=false) + protected long dataCenterId; + + @Column(name="account_id") + protected Long accountId; + + public Date getTakenAt() { + return takenAt; + } + + public void setTakenAt(Date taken) { + this.takenAt = taken; + } + + public DataCenterVnetVO(String vnet, long dcId) { + this.vnet = vnet; + this.dataCenterId = dcId; + this.takenAt = null; + } + + public Long getId() { + return id; + } + + public String getVnet() { + return vnet; + } + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public long getDataCenterId() { + return dataCenterId; + } + + protected DataCenterVnetVO() { + } +} diff --git a/core/src/com/cloud/dc/HostPodVO.java b/core/src/com/cloud/dc/HostPodVO.java new file mode 100644 index 00000000000..25f4a227bdd --- /dev/null +++ b/core/src/com/cloud/dc/HostPodVO.java @@ -0,0 +1,140 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.utils.NumbersUtil; + +@Entity +@Table(name = "host_pod_ref") +public class HostPodVO { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + long id; + + @Column(name = "name") + private String name = null; + + @Column(name = "data_center_id") + private long dataCenterId; + + @Column(name = "gateway") + private String gateway; + + @Column(name = "cidr_address") + private String cidrAddress; + + @Column(name = "cidr_size") + private long cidrSize; + + @Column(name = "description") + private String description; + + public HostPodVO(String name, long dcId, String gateway, String cidrAddress, long cidrSize, String description) { + this.name = name; + this.dataCenterId = dcId; + this.gateway = gateway; + this.cidrAddress = cidrAddress; + this.cidrSize = cidrSize; + this.description = description; + } + + /* + * public HostPodVO(String name, long dcId) { this(null, name, dcId); } + */ + protected HostPodVO() { + } + + public long getId() { + return id; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public void setDataCenterId(long dataCenterId) { + this.dataCenterId = dataCenterId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCidrAddress() { + return cidrAddress; + } + + public void setCidrAddress(String cidrAddress) { + this.cidrAddress = cidrAddress; + } + + public long getCidrSize() { + return cidrSize; + } + + public void setCidrSize(long cidrSize) { + this.cidrSize = cidrSize; + } + + public String getGateway() { + return gateway; + } + + public void setGateway(String gateway) { + this.gateway = gateway; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + // Use for comparisons only. + public HostPodVO(Long id) { + this.id = id; + } + + @Override + public int hashCode() { + return NumbersUtil.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof HostPodVO) { + return id == ((HostPodVO)obj).id; + } else { + return false; + } + } +} diff --git a/core/src/com/cloud/dc/PodCluster.java b/core/src/com/cloud/dc/PodCluster.java new file mode 100644 index 00000000000..ad2c61cea65 --- /dev/null +++ b/core/src/com/cloud/dc/PodCluster.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc; + +public class PodCluster { + HostPodVO _pod; + ClusterVO _cluster; + + protected PodCluster() { + super(); + } + + public PodCluster(HostPodVO pod, ClusterVO cluster) { + _pod = pod; + _cluster = cluster; + } + + public HostPodVO getPod() { + return _pod; + } + + public ClusterVO getCluster() { + return _cluster; + } + + + @Override + public int hashCode() { + return _pod.hashCode() ^ (_cluster != null ? _cluster.hashCode() : 0); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PodCluster)) { + return false; + } + + PodCluster that = (PodCluster)obj; + if (!this._pod.equals(that._pod)) { + return false; + } + + if (this._cluster == null && that._cluster == null) { + return true; + } + + if (this._cluster == null || that._cluster == null) { + return false; + } + + return this._cluster.equals(that._cluster); + } +} diff --git a/core/src/com/cloud/dc/PodVlanMapVO.java b/core/src/com/cloud/dc/PodVlanMapVO.java new file mode 100644 index 00000000000..97f88238551 --- /dev/null +++ b/core/src/com/cloud/dc/PodVlanMapVO.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="pod_vlan_map") +public class PodVlanMapVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="pod_id") + private long podId; + + @Column(name="vlan_db_id") + private long vlanDbId; + + public PodVlanMapVO(long podId, long vlanDbId) { + this.podId = podId; + this.vlanDbId = vlanDbId; + } + + public PodVlanMapVO() { + + } + + public Long getId() { + return id; + } + + public long getPodId() { + return podId; + } + + public long getVlanDbId() { + return vlanDbId; + } +} diff --git a/core/src/com/cloud/dc/PodVlanVO.java b/core/src/com/cloud/dc/PodVlanVO.java new file mode 100755 index 00000000000..63118b5d6a3 --- /dev/null +++ b/core/src/com/cloud/dc/PodVlanVO.java @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="op_pod_vlan_alloc") +public class PodVlanVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + Long id; + + @Column(name="taken", nullable=true) + @Temporal(value=TemporalType.TIMESTAMP) + Date takenAt; + + @Column(name="vlan", updatable=false, nullable=false) + protected String vlan; + + @Column(name="data_center_id") + long dataCenterId; + + @Column(name="pod_id", updatable=false, nullable=false) + protected long podId; + + @Column(name="account_id") + protected Long accountId; + + public Date getTakenAt() { + return takenAt; + } + + public void setTakenAt(Date taken) { + this.takenAt = taken; + } + + public PodVlanVO(String vlan, long dataCenterId, long podId) { + this.vlan = vlan; + this.dataCenterId = dataCenterId; + this.podId = podId; + this.takenAt = null; + } + + public Long getId() { + return id; + } + + public Long getAccountId() { + return accountId; + } + + public String getVlan() { + return vlan; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public long getPodId() { + return podId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + protected PodVlanVO() { + } +} diff --git a/core/src/com/cloud/dc/Vlan.java b/core/src/com/cloud/dc/Vlan.java new file mode 100644 index 00000000000..5182310b250 --- /dev/null +++ b/core/src/com/cloud/dc/Vlan.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc; + +public interface Vlan { + public enum VlanType { + DirectAttached, + VirtualNetwork + } + + public final static String UNTAGGED = "untagged"; + + public Long getId(); + + public String getVlanId(); + + public String getVlanGateway(); + + public String getVlanNetmask(); + + public long getDataCenterId(); + + public void setIpRange(String description); + + public String getIpRange(); + + public void setVlanType(VlanType ipRange); + + public VlanType getVlanType(); + +} \ No newline at end of file diff --git a/core/src/com/cloud/dc/VlanVO.java b/core/src/com/cloud/dc/VlanVO.java new file mode 100644 index 00000000000..1cb991b5f92 --- /dev/null +++ b/core/src/com/cloud/dc/VlanVO.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="vlan") +public class VlanVO implements Vlan { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + Long id; + + @Column(name="vlan_id") + String vlanId; + + @Column(name="vlan_gateway") + String vlanGateway; + + @Column(name="vlan_netmask") + String vlanNetmask; + + @Column(name="data_center_id") + long dataCenterId; + + @Column(name="description") + String ipRange; + + @Column(name="vlan_type") + @Enumerated(EnumType.STRING) + VlanType vlanType; + + public VlanVO(VlanType vlanType, String vlanTag, String vlanGateway, String vlanNetmask, long dataCenterId, String ipRange) { + this.vlanType = vlanType; + this.vlanId = vlanTag; + this.vlanGateway = vlanGateway; + this.vlanNetmask = vlanNetmask; + this.dataCenterId = dataCenterId; + this.ipRange = ipRange; + } + + public VlanVO() { + + } + + public Long getId() { + return id; + } + + public String getVlanId() { + return vlanId; + } + + public String getVlanGateway() { + return vlanGateway; + } + + public void setVlanGateway(String vlanGateway) { + this.vlanGateway = vlanGateway; + } + + public String getVlanNetmask() { + return vlanNetmask; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public void setIpRange(String ipRange) { + this.ipRange = ipRange; + } + + public String getIpRange() { + return ipRange; + } + + public void setVlanType(VlanType vlanType) { + this.vlanType = vlanType; + } + + public VlanType getVlanType() { + return vlanType; + } +} diff --git a/core/src/com/cloud/dc/dao/AccountVlanMapDao.java b/core/src/com/cloud/dc/dao/AccountVlanMapDao.java new file mode 100644 index 00000000000..df150e8ff9c --- /dev/null +++ b/core/src/com/cloud/dc/dao/AccountVlanMapDao.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc.dao; + +import java.util.List; + +import com.cloud.dc.AccountVlanMapVO; +import com.cloud.utils.db.GenericDao; + +public interface AccountVlanMapDao extends GenericDao { + + public List listAccountVlanMapsByAccount(long accountId); + public List listAccountVlanMapsByVlan(long vlanDbId); + public AccountVlanMapVO findAccountVlanMap(long accountId, long vlanDbId); + +} diff --git a/core/src/com/cloud/dc/dao/AccountVlanMapDaoImpl.java b/core/src/com/cloud/dc/dao/AccountVlanMapDaoImpl.java new file mode 100644 index 00000000000..a59d622090e --- /dev/null +++ b/core/src/com/cloud/dc/dao/AccountVlanMapDaoImpl.java @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.dc.AccountVlanMapVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={AccountVlanMapDao.class}) +public class AccountVlanMapDaoImpl extends GenericDaoBase implements AccountVlanMapDao { + + protected SearchBuilder AccountSearch; + protected SearchBuilder VlanSearch; + protected SearchBuilder AccountVlanSearch; + + @Override + public List listAccountVlanMapsByAccount(long accountId) { + SearchCriteria sc = AccountSearch.create(); + sc.setParameters("accountId", accountId); + return listBy(sc); + } + + @Override + public List listAccountVlanMapsByVlan(long vlanDbId) { + SearchCriteria sc = VlanSearch.create(); + sc.setParameters("vlanDbId", vlanDbId); + return listBy(sc); + } + + @Override + public AccountVlanMapVO findAccountVlanMap(long accountId, long vlanDbId) { + SearchCriteria sc = AccountVlanSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("vlanDbId", vlanDbId); + return findOneBy(sc); + } + + public AccountVlanMapDaoImpl() { + AccountSearch = createSearchBuilder(); + AccountSearch.and("accountId", AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountSearch.done(); + + VlanSearch = createSearchBuilder(); + VlanSearch.and("vlanDbId", VlanSearch.entity().getVlanDbId(), SearchCriteria.Op.EQ); + VlanSearch.done(); + + AccountVlanSearch = createSearchBuilder(); + AccountVlanSearch.and("accountId", AccountVlanSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountVlanSearch.and("vlanDbId", AccountVlanSearch.entity().getVlanDbId(), SearchCriteria.Op.EQ); + AccountVlanSearch.done(); + } + +} diff --git a/core/src/com/cloud/dc/dao/ClusterDao.java b/core/src/com/cloud/dc/dao/ClusterDao.java new file mode 100644 index 00000000000..759a566ac26 --- /dev/null +++ b/core/src/com/cloud/dc/dao/ClusterDao.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc.dao; + +import java.util.List; + +import com.cloud.dc.ClusterVO; +import com.cloud.utils.db.GenericDao; + +public interface ClusterDao extends GenericDao { + List listByPodId(long podId); + ClusterVO findBy(String name, long podId); +} diff --git a/core/src/com/cloud/dc/dao/ClusterDaoImpl.java b/core/src/com/cloud/dc/dao/ClusterDaoImpl.java new file mode 100644 index 00000000000..e1d8ffe4b05 --- /dev/null +++ b/core/src/com/cloud/dc/dao/ClusterDaoImpl.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.dc.ClusterVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value=ClusterDao.class) +public class ClusterDaoImpl extends GenericDaoBase implements ClusterDao { + + protected final SearchBuilder PodSearch; + + protected ClusterDaoImpl() { + super(); + + PodSearch = createSearchBuilder(); + PodSearch.and("pod", PodSearch.entity().getPodId(), SearchCriteria.Op.EQ); + PodSearch.and("name", PodSearch.entity().getName(), SearchCriteria.Op.EQ); + PodSearch.done(); + } + + @Override + public List listByPodId(long podId) { + SearchCriteria sc = PodSearch.create(); + sc.setParameters("pod", podId); + + return listActiveBy(sc); + } + + @Override + public ClusterVO findBy(String name, long podId) { + SearchCriteria sc = PodSearch.create(); + sc.setParameters("pod", podId); + sc.setParameters("name", name); + + return findOneActiveBy(sc); + } +} diff --git a/core/src/com/cloud/dc/dao/DataCenterDao.java b/core/src/com/cloud/dc/dao/DataCenterDao.java new file mode 100644 index 00000000000..8075e3b34bc --- /dev/null +++ b/core/src/com/cloud/dc/dao/DataCenterDao.java @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc.dao; + +import java.util.List; + +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.DataCenterVnetVO; +import com.cloud.utils.db.GenericDao; + +public interface DataCenterDao extends GenericDao { + DataCenterVO findByName(String name); + + /** + * @param id data center id + * @return a pair of mac address strings. The first one is private and second is public. + */ + String[] getNextAvailableMacAddressPair(long id); + String[] getNextAvailableMacAddressPair(long id, long mask); + String allocatePrivateIpAddress(long id, long podId, long instanceId); + String allocateLinkLocalPrivateIpAddress(long id, long podId, long instanceId); + String allocateVnet(long dcId, long accountId); + + void releaseVnet(String vnet, long dcId, long accountId); + void releasePrivateIpAddress(String ipAddress, long dcId, Long instanceId); + void releaseLinkLocalPrivateIpAddress(String ipAddress, long dcId, Long instanceId); + boolean deletePrivateIpAddressByPod(long podId); + boolean deleteLinkLocalPrivateIpAddressByPod(long podId); + + void addPrivateIpAddress(long dcId,long podId, String start, String end); + void addLinkLocalPrivateIpAddress(long dcId,long podId, String start, String end); + + List findVnet(long dcId, String vnet); + + void addVnet(long dcId, int start, int end); + + void deleteVnet(long dcId); + + List listAllocatedVnets(long dcId); + + String allocatePodVlan(long podId, long accountId); +} diff --git a/core/src/com/cloud/dc/dao/DataCenterDaoImpl.java b/core/src/com/cloud/dc/dao/DataCenterDaoImpl.java new file mode 100644 index 00000000000..a7d4b088ce8 --- /dev/null +++ b/core/src/com/cloud/dc/dao/DataCenterDaoImpl.java @@ -0,0 +1,211 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc.dao; + +import java.util.List; +import java.util.Map; +import java.util.Random; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; +import javax.persistence.TableGenerator; + +import org.apache.log4j.Logger; + +import com.cloud.dc.DataCenterIpAddressVO; +import com.cloud.dc.DataCenterLinkLocalIpAddressVO; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.DataCenterVnetVO; +import com.cloud.dc.PodVlanVO; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.SequenceFetcher; +import com.cloud.utils.net.NetUtils; + +/** + * @config + * {@table + * || Param Name | Description | Values | Default || + * || mac.address.prefix | prefix to attach to all public and private mac addresses | number | 06 || + * } + **/ +@Local(value={DataCenterDao.class}) +public class DataCenterDaoImpl extends GenericDaoBase implements DataCenterDao { + private static final Logger s_logger = Logger.getLogger(DataCenterDaoImpl.class); + + protected SearchBuilder NameSearch; + + protected static final DataCenterIpAddressDaoImpl _ipAllocDao = ComponentLocator.inject(DataCenterIpAddressDaoImpl.class); + protected static final DataCenterLinkLocalIpAddressDaoImpl _LinkLocalIpAllocDao = ComponentLocator.inject(DataCenterLinkLocalIpAddressDaoImpl.class); + protected static final DataCenterVnetDaoImpl _vnetAllocDao = ComponentLocator.inject(DataCenterVnetDaoImpl.class); + protected static final PodVlanDaoImpl _podVlanAllocDao = ComponentLocator.inject(PodVlanDaoImpl.class); + protected long _prefix; + protected Random _rand = new Random(System.currentTimeMillis()); + protected TableGenerator _tgMacAddress; + + @Override + public DataCenterVO findByName(String name) { + SearchCriteria sc = NameSearch.create(); + sc.setParameters("name", name); + return findOneActiveBy(sc); + } + + @Override + public void releaseVnet(String vnet, long dcId, long accountId) { + _vnetAllocDao.release(vnet, dcId, accountId); + } + + @Override + public List findVnet(long dcId, String vnet) { + return _vnetAllocDao.findVnet(dcId, vnet); + } + + @Override + public void releasePrivateIpAddress(String ipAddress, long dcId, Long instanceId) { + _ipAllocDao.releaseIpAddress(ipAddress, dcId, instanceId); + } + + @Override + public void releaseLinkLocalPrivateIpAddress(String ipAddress, long dcId, Long instanceId) { + _LinkLocalIpAllocDao.releaseIpAddress(ipAddress, dcId, instanceId); + } + + @Override + public boolean deletePrivateIpAddressByPod(long podId) { + return _ipAllocDao.deleteIpAddressByPod(podId); + } + + @Override + public boolean deleteLinkLocalPrivateIpAddressByPod(long podId) { + return _LinkLocalIpAllocDao.deleteIpAddressByPod(podId); + } + + @Override + public String allocateVnet(long dataCenterId, long accountId) { + DataCenterVnetVO vo = _vnetAllocDao.take(dataCenterId, accountId); + if (vo == null) { + return null; + } + + return vo.getVnet(); + } + + @Override + public String allocatePodVlan(long podId, long accountId) { + PodVlanVO vo = _podVlanAllocDao.take(podId, accountId); + if (vo == null) { + return null; + } + return vo.getVlan(); + } + + @Override + public String[] getNextAvailableMacAddressPair(long id) { + return getNextAvailableMacAddressPair(id, 0); + } + + @Override + public String[] getNextAvailableMacAddressPair(long id, long mask) { + SequenceFetcher fetch = SequenceFetcher.getInstance(); + + long seq = fetch.getNextSequence(Long.class, _tgMacAddress, id); + seq = seq | _prefix | ((id & 0x7f) << 32); + seq |= mask; + seq |= ((_rand.nextInt(Short.MAX_VALUE) << 16) & 0x00000000ffff0000l); + String[] pair = new String[2]; + pair[0] = NetUtils.long2Mac(seq); + pair[1] = NetUtils.long2Mac(seq | 0x1l << 39); + return pair; + } + + @Override + public String allocatePrivateIpAddress(long dcId, long podId, long instanceId) { + DataCenterIpAddressVO vo = _ipAllocDao.takeIpAddress(dcId, podId, instanceId); + if (vo == null) { + return null; + } + return vo.getIpAddress(); + } + + @Override + public String allocateLinkLocalPrivateIpAddress(long dcId, long podId, long instanceId) { + DataCenterLinkLocalIpAddressVO vo = _LinkLocalIpAllocDao.takeIpAddress(dcId, podId, instanceId); + if (vo == null) { + return null; + } + return vo.getIpAddress(); + } + + @Override + public void addVnet(long dcId, int start, int end) { + _vnetAllocDao.add(dcId, start, end); + } + + @Override + public void deleteVnet(long dcId) { + _vnetAllocDao.delete(dcId); + } + + @Override + public List listAllocatedVnets(long dcId) { + return _vnetAllocDao.listAllocatedVnets(dcId); + } + + @Override + public void addPrivateIpAddress(long dcId,long podId, String start, String end) { + _ipAllocDao.addIpRange(dcId, podId, start, end); + } + + @Override + public void addLinkLocalPrivateIpAddress(long dcId,long podId, String start, String end) { + _LinkLocalIpAllocDao.addIpRange(dcId, podId, start, end); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + if (!super.configure(name, params)) { + return false; + } + + String value = (String)params.get("mac.address.prefix"); + _prefix = (long)NumbersUtil.parseInt(value, 06) << 40; + + if (!_ipAllocDao.configure("Ip Alloc", params)) { + return false; + } + + if (!_vnetAllocDao.configure("vnet Alloc", params)) { + return false; + } + return true; + } + + protected DataCenterDaoImpl() { + super(); + NameSearch = createSearchBuilder(); + NameSearch.and("name", NameSearch.entity().getName(), SearchCriteria.Op.EQ); + NameSearch.done(); + + _tgMacAddress = _tgs.get("macAddress"); + assert _tgMacAddress != null : "Couldn't get mac address table generator"; + } +} diff --git a/core/src/com/cloud/dc/dao/DataCenterIpAddressDaoImpl.java b/core/src/com/cloud/dc/dao/DataCenterIpAddressDaoImpl.java new file mode 100755 index 00000000000..06cb6726af8 --- /dev/null +++ b/core/src/com/cloud/dc/dao/DataCenterIpAddressDaoImpl.java @@ -0,0 +1,208 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.dc.DataCenterIpAddressVO; +import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; + +@Local(value={DataCenterIpAddressDaoImpl.class}) +public class DataCenterIpAddressDaoImpl extends GenericDaoBase implements GenericDao { + private static final Logger s_logger = Logger.getLogger(DataCenterIpAddressDaoImpl.class); + + private static final String COUNT_ALL_PRIVATE_IPS = "SELECT count(*) from `cloud`.`op_dc_ip_address_alloc` where pod_id = ? AND data_center_id = ?"; + private static final String COUNT_ALLOCATED_PRIVATE_IPS = "SELECT count(*) from `cloud`.`op_dc_ip_address_alloc` where pod_id = ? AND data_center_id = ? AND taken is not null"; + + private final SearchBuilder FreeIpSearch; + private final SearchBuilder IpDcSearch; + private final SearchBuilder PodDcSearch; + private final SearchBuilder PodDcIpSearch; + private final SearchBuilder FreePodDcIpSearch; + + public DataCenterIpAddressVO takeIpAddress(long dcId, long podId, long instanceId) { + SearchCriteria sc = FreeIpSearch.create(); + sc.setParameters("dc", dcId); + sc.setParameters("pod", podId); + + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + + DataCenterIpAddressVO vo = lock(sc, true); + if (vo == null) { + txn.rollback(); + return vo; + } + vo.setTakenAt(new Date()); + vo.setInstanceId(instanceId); + update(vo.getId(), vo); + txn.commit(); + return vo; + } catch (Exception e) { + txn.rollback(); + throw new CloudRuntimeException("Caught Exception ", e); + } + } + + public boolean deleteIpAddressByPod(long podId) { + Transaction txn = Transaction.currentTxn(); + try { + String deleteSql = "DELETE FROM `cloud`.`op_dc_ip_address_alloc` WHERE `pod_id` = ?"; + PreparedStatement stmt = txn.prepareAutoCloseStatement(deleteSql); + stmt.setLong(1, podId); + return stmt.execute(); + } catch(Exception e) { + throw new CloudRuntimeException("Caught Exception ", e); + } + } + + public boolean mark(long dcId, long podId, String ip) { + SearchCriteria sc = FreePodDcIpSearch.create(); + sc.setParameters("podId", podId); + sc.setParameters("dcId", dcId); + sc.setParameters("ipAddress", ip); + + DataCenterIpAddressVO vo = createForUpdate(); + vo.setTakenAt(new Date()); + + return update(vo, sc) >= 1; + } + + public void addIpRange(long dcId, long podId, String start, String end) { + Transaction txn = Transaction.currentTxn(); + String insertSql = "INSERT INTO `cloud`.`op_dc_ip_address_alloc` (ip_address, data_center_id, pod_id) VALUES (?, ?, ?)"; + PreparedStatement stmt = null; + + long startIP = NetUtils.ip2Long(start); + long endIP = NetUtils.ip2Long(end); + + while (startIP <= endIP) { + try { + stmt = txn.prepareAutoCloseStatement(insertSql); + stmt.setString(1, NetUtils.long2Ip(startIP)); + stmt.setLong(2, dcId); + stmt.setLong(3, podId); + stmt.executeUpdate(); + stmt.close(); + } catch (Exception ex) { + s_logger.warn("Unable to persist " + NetUtils.long2Ip(startIP) + " due to " + ex.getMessage()); + } + startIP++; + } + } + + public void releaseIpAddress(String ipAddress, long dcId, Long instanceId) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Releasing ip address: " + ipAddress + " data center " + dcId); + } + SearchCriteria sc = IpDcSearch.create(); + sc.setParameters("ip", ipAddress); + sc.setParameters("dc", dcId); + sc.setParameters("instance", instanceId); + + DataCenterIpAddressVO vo = createForUpdate(); + + vo.setTakenAt(null); + vo.setInstanceId(null); + update(vo, sc); + } + + protected DataCenterIpAddressDaoImpl() { + super(); + FreeIpSearch = createSearchBuilder(); + FreeIpSearch.and("dc", FreeIpSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + FreeIpSearch.and("pod", FreeIpSearch.entity().getPodId(), SearchCriteria.Op.EQ); + FreeIpSearch.and("taken", FreeIpSearch.entity().getTakenAt(), SearchCriteria.Op.NULL); + FreeIpSearch.done(); + + IpDcSearch = createSearchBuilder(); + IpDcSearch.and("ip", IpDcSearch.entity().getIpAddress(), SearchCriteria.Op.EQ); + IpDcSearch.and("dc", IpDcSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + IpDcSearch.and("instance", IpDcSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + IpDcSearch.done(); + + PodDcSearch = createSearchBuilder(); + PodDcSearch.and("podId", PodDcSearch.entity().getPodId(), SearchCriteria.Op.EQ); + PodDcSearch.and("dataCenterId", PodDcSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + PodDcSearch.done(); + + PodDcIpSearch = createSearchBuilder(); + PodDcIpSearch.and("dcId", PodDcIpSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + PodDcIpSearch.and("podId", PodDcIpSearch.entity().getPodId(), SearchCriteria.Op.EQ); + PodDcIpSearch.and("ipAddress", PodDcIpSearch.entity().getIpAddress(), SearchCriteria.Op.EQ); + PodDcIpSearch.done(); + + FreePodDcIpSearch = createSearchBuilder(); + FreePodDcIpSearch.and("dcId", FreePodDcIpSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + FreePodDcIpSearch.and("podId", FreePodDcIpSearch.entity().getPodId(), SearchCriteria.Op.EQ); + FreePodDcIpSearch.and("ipAddress", FreePodDcIpSearch.entity().getIpAddress(), SearchCriteria.Op.EQ); + FreePodDcIpSearch.and("taken", FreePodDcIpSearch.entity().getTakenAt(), SearchCriteria.Op.EQ); + FreePodDcIpSearch.done(); + } + + public List listByPodIdDcId(long podId, long dcId) { + SearchCriteria sc = PodDcSearch.create(); + sc.setParameters("podId", podId); + sc.setParameters("dataCenterId", dcId); + return listBy(sc); + } + + public List listByPodIdDcIdIpAddress(long podId, long dcId, String ipAddress) { + SearchCriteria sc = PodDcIpSearch.create(); + sc.setParameters("dcId", dcId); + sc.setParameters("podId", podId); + sc.setParameters("ipAddress", ipAddress); + return listBy(sc); + } + + public int countIPs(long podId, long dcId, boolean onlyCountAllocated) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + int ipCount = 0; + try { + String sql = ""; + if (onlyCountAllocated) sql = COUNT_ALLOCATED_PRIVATE_IPS; + else sql = COUNT_ALL_PRIVATE_IPS; + + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, podId); + pstmt.setLong(2, dcId); + ResultSet rs = pstmt.executeQuery(); + + if (rs.next()) ipCount = rs.getInt(1); + + } catch (Exception e) { + s_logger.warn("Exception searching for routers and proxies", e); + } + return ipCount; + } +} diff --git a/core/src/com/cloud/dc/dao/DataCenterLinkLocalIpAddressDaoImpl.java b/core/src/com/cloud/dc/dao/DataCenterLinkLocalIpAddressDaoImpl.java new file mode 100644 index 00000000000..359cd6d0958 --- /dev/null +++ b/core/src/com/cloud/dc/dao/DataCenterLinkLocalIpAddressDaoImpl.java @@ -0,0 +1,208 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.dc.DataCenterLinkLocalIpAddressVO; +import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; + +@Local(value={DataCenterLinkLocalIpAddressDaoImpl.class}) +public class DataCenterLinkLocalIpAddressDaoImpl extends GenericDaoBase implements GenericDao { + private static final Logger s_logger = Logger.getLogger(DataCenterLinkLocalIpAddressDaoImpl.class); + + private static final String COUNT_ALL_PRIVATE_IPS = "SELECT count(*) from `cloud`.`op_dc_link_local_ip_address_alloc` where pod_id = ? AND data_center_id = ?"; + private static final String COUNT_ALLOCATED_PRIVATE_IPS = "SELECT count(*) from `cloud`.`op_dc_link_local_ip_address_alloc` where pod_id = ? AND data_center_id = ? AND taken is not null"; + + private final SearchBuilder FreeIpSearch; + private final SearchBuilder IpDcSearch; + private final SearchBuilder PodDcSearch; + private final SearchBuilder PodDcIpSearch; + private final SearchBuilder FreePodDcIpSearch; + + public DataCenterLinkLocalIpAddressVO takeIpAddress(long dcId, long podId, long instanceId) { + SearchCriteria sc = FreeIpSearch.create(); + sc.setParameters("dc", dcId); + sc.setParameters("pod", podId); + + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + + DataCenterLinkLocalIpAddressVO vo = lock(sc, true); + if (vo == null) { + txn.rollback(); + return vo; + } + vo.setTakenAt(new Date()); + vo.setInstanceId(instanceId); + update(vo.getId(), vo); + txn.commit(); + return vo; + } catch (Exception e) { + txn.rollback(); + throw new CloudRuntimeException("Caught Exception ", e); + } + } + + public boolean deleteIpAddressByPod(long podId) { + Transaction txn = Transaction.currentTxn(); + try { + String deleteSql = "DELETE FROM `cloud`.`op_dc_link_local_ip_address_alloc` WHERE `pod_id` = ?"; + PreparedStatement stmt = txn.prepareAutoCloseStatement(deleteSql); + stmt.setLong(1, podId); + return stmt.execute(); + } catch(Exception e) { + throw new CloudRuntimeException("Caught Exception ", e); + } + } + + public boolean mark(long dcId, long podId, String ip) { + SearchCriteria sc = FreePodDcIpSearch.create(); + sc.setParameters("podId", podId); + sc.setParameters("dcId", dcId); + sc.setParameters("ipAddress", ip); + + DataCenterLinkLocalIpAddressVO vo = createForUpdate(); + vo.setTakenAt(new Date()); + + return update(vo, sc) >= 1; + } + + public void addIpRange(long dcId, long podId, String start, String end) { + Transaction txn = Transaction.currentTxn(); + String insertSql = "INSERT INTO `cloud`.`op_dc_link_local_ip_address_alloc` (ip_address, data_center_id, pod_id) VALUES (?, ?, ?)"; + PreparedStatement stmt = null; + + long startIP = NetUtils.ip2Long(start); + long endIP = NetUtils.ip2Long(end); + + while (startIP <= endIP) { + try { + stmt = txn.prepareAutoCloseStatement(insertSql); + stmt.setString(1, NetUtils.long2Ip(startIP)); + stmt.setLong(2, dcId); + stmt.setLong(3, podId); + stmt.executeUpdate(); + stmt.close(); + } catch (Exception ex) { + s_logger.warn("Unable to persist " + NetUtils.long2Ip(startIP) + " due to " + ex.getMessage()); + } + startIP++; + } + } + + public void releaseIpAddress(String ipAddress, long dcId, Long instanceId) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Releasing ip address: " + ipAddress + " data center " + dcId); + } + SearchCriteria sc = IpDcSearch.create(); + sc.setParameters("ip", ipAddress); + sc.setParameters("dc", dcId); + sc.setParameters("instance", instanceId); + + DataCenterLinkLocalIpAddressVO vo = createForUpdate(); + + vo.setTakenAt(null); + vo.setInstanceId(null); + update(vo, sc); + } + + protected DataCenterLinkLocalIpAddressDaoImpl() { + super(); + FreeIpSearch = createSearchBuilder(); + FreeIpSearch.and("dc", FreeIpSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + FreeIpSearch.and("pod", FreeIpSearch.entity().getPodId(), SearchCriteria.Op.EQ); + FreeIpSearch.and("taken", FreeIpSearch.entity().getTakenAt(), SearchCriteria.Op.NULL); + FreeIpSearch.done(); + + IpDcSearch = createSearchBuilder(); + IpDcSearch.and("ip", IpDcSearch.entity().getIpAddress(), SearchCriteria.Op.EQ); + IpDcSearch.and("dc", IpDcSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + IpDcSearch.and("instance", IpDcSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + IpDcSearch.done(); + + PodDcSearch = createSearchBuilder(); + PodDcSearch.and("podId", PodDcSearch.entity().getPodId(), SearchCriteria.Op.EQ); + PodDcSearch.and("dataCenterId", PodDcSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + PodDcSearch.done(); + + PodDcIpSearch = createSearchBuilder(); + PodDcIpSearch.and("dcId", PodDcIpSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + PodDcIpSearch.and("podId", PodDcIpSearch.entity().getPodId(), SearchCriteria.Op.EQ); + PodDcIpSearch.and("ipAddress", PodDcIpSearch.entity().getIpAddress(), SearchCriteria.Op.EQ); + PodDcIpSearch.done(); + + FreePodDcIpSearch = createSearchBuilder(); + FreePodDcIpSearch.and("dcId", FreePodDcIpSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + FreePodDcIpSearch.and("podId", FreePodDcIpSearch.entity().getPodId(), SearchCriteria.Op.EQ); + FreePodDcIpSearch.and("ipAddress", FreePodDcIpSearch.entity().getIpAddress(), SearchCriteria.Op.EQ); + FreePodDcIpSearch.and("taken", FreePodDcIpSearch.entity().getTakenAt(), SearchCriteria.Op.EQ); + FreePodDcIpSearch.done(); + } + + public List listByPodIdDcId(long podId, long dcId) { + SearchCriteria sc = PodDcSearch.create(); + sc.setParameters("podId", podId); + sc.setParameters("dataCenterId", dcId); + return listBy(sc); + } + + public List listByPodIdDcIdIpAddress(long podId, long dcId, String ipAddress) { + SearchCriteria sc = PodDcIpSearch.create(); + sc.setParameters("dcId", dcId); + sc.setParameters("podId", podId); + sc.setParameters("ipAddress", ipAddress); + return listBy(sc); + } + + public int countIPs(long podId, long dcId, boolean onlyCountAllocated) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + int ipCount = 0; + try { + String sql = ""; + if (onlyCountAllocated) sql = COUNT_ALLOCATED_PRIVATE_IPS; + else sql = COUNT_ALL_PRIVATE_IPS; + + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, podId); + pstmt.setLong(2, dcId); + ResultSet rs = pstmt.executeQuery(); + + if (rs.next()) ipCount = rs.getInt(1); + + } catch (Exception e) { + s_logger.warn("Exception searching for routers and proxies", e); + } + return ipCount; + } +} diff --git a/core/src/com/cloud/dc/dao/DataCenterVnetDaoImpl.java b/core/src/com/cloud/dc/dao/DataCenterVnetDaoImpl.java new file mode 100755 index 00000000000..e641c6c62a2 --- /dev/null +++ b/core/src/com/cloud/dc/dao/DataCenterVnetDaoImpl.java @@ -0,0 +1,151 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc.dao; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Date; +import java.util.List; + +import com.cloud.dc.DataCenterVnetVO; +import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.exception.CloudRuntimeException; + +/** + * DataCenterVnetDaoImpl maintains the one-to-many relationship between + * data center and the vnet that appears within its network. + */ +public class DataCenterVnetDaoImpl extends GenericDaoBase implements GenericDao { + private final SearchBuilder FreeVnetSearch; + private final SearchBuilder VnetDcSearch; + private final SearchBuilder VnetDcSearchAllocated; + private final SearchBuilder DcSearchAllocated; + + public List listAllocatedVnets(long dcId) { + SearchCriteria sc = DcSearchAllocated.create(); + sc.setParameters("dc", dcId); + return listActiveBy(sc); + } + + public List findVnet(long dcId, String vnet) { + SearchCriteria sc = VnetDcSearch.create();; + sc.setParameters("dc", dcId); + sc.setParameters("vnet", vnet); + return listActiveBy(sc); + } + + public void add(long dcId, int start, int end) { + String insertVnet = "INSERT INTO `cloud`.`op_dc_vnet_alloc` (vnet, data_center_id) VALUES ( ?, ?)"; + + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + PreparedStatement stmt = txn.prepareAutoCloseStatement(insertVnet); + for (int i = start; i < end; i++) { + stmt.setString(1, String.valueOf(i)); + stmt.setLong(2, dcId); + stmt.addBatch(); + } + stmt.executeBatch(); + txn.commit(); + } catch (SQLException e) { + throw new CloudRuntimeException("Exception caught adding vnet ", e); + } + } + + public void delete(long dcId) { + String deleteVnet = "DELETE FROM `cloud`.`op_dc_vnet_alloc` WHERE data_center_id = ?"; + + Transaction txn = Transaction.currentTxn(); + try { + PreparedStatement stmt = txn.prepareAutoCloseStatement(deleteVnet); + stmt.setLong(1, dcId); + stmt.executeUpdate(); + } catch (SQLException e) { + throw new CloudRuntimeException("Exception caught deleting vnet ", e); + } + } + + public DataCenterVnetVO take(long dcId, long accountId) { + SearchCriteria sc = FreeVnetSearch.create(); + sc.setParameters("dc", dcId); + Date now = new Date(); + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + DataCenterVnetVO vo = lock(sc, true); + if (vo == null) { + return null; + } + + vo.setTakenAt(now); + vo.setAccountId(accountId); + update(vo.getId(), vo); + txn.commit(); + return vo; + + } catch (Exception e) { + throw new CloudRuntimeException("Caught Exception ", e); + } + } + + public void release(String vnet, long dcId, long accountId) { + SearchCriteria sc = VnetDcSearchAllocated.create(); + sc.setParameters("vnet", vnet); + sc.setParameters("dc", dcId); + sc.setParameters("account", accountId); + + DataCenterVnetVO vo = findOneBy(sc); + if (vo == null) { + return; + } + + vo.setTakenAt(null); + vo.setAccountId(null); + update(vo.getId(), vo); + } + + protected DataCenterVnetDaoImpl() { + super(); + DcSearchAllocated = createSearchBuilder(); + DcSearchAllocated.and("dc", DcSearchAllocated.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DcSearchAllocated.and("allocated", DcSearchAllocated.entity().getTakenAt(), SearchCriteria.Op.NNULL); + DcSearchAllocated.done(); + + FreeVnetSearch = createSearchBuilder(); + FreeVnetSearch.and("dc", FreeVnetSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + FreeVnetSearch.and("taken", FreeVnetSearch.entity().getTakenAt(), SearchCriteria.Op.NULL); + FreeVnetSearch.done(); + + VnetDcSearch = createSearchBuilder(); + VnetDcSearch.and("vnet", VnetDcSearch.entity().getVnet(), SearchCriteria.Op.EQ); + VnetDcSearch.and("dc", VnetDcSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + VnetDcSearch.done(); + + VnetDcSearchAllocated = createSearchBuilder(); + VnetDcSearchAllocated.and("vnet", VnetDcSearchAllocated.entity().getVnet(), SearchCriteria.Op.EQ); + VnetDcSearchAllocated.and("dc", VnetDcSearchAllocated.entity().getDataCenterId(), SearchCriteria.Op.EQ); + VnetDcSearchAllocated.and("taken", VnetDcSearchAllocated.entity().getTakenAt(), SearchCriteria.Op.NNULL); + VnetDcSearchAllocated.and("account", VnetDcSearchAllocated.entity().getAccountId(), SearchCriteria.Op.EQ); + VnetDcSearchAllocated.done(); + } +} diff --git a/core/src/com/cloud/dc/dao/HostPodDao.java b/core/src/com/cloud/dc/dao/HostPodDao.java new file mode 100644 index 00000000000..01fb2864819 --- /dev/null +++ b/core/src/com/cloud/dc/dao/HostPodDao.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc.dao; + +import java.util.HashMap; +import java.util.List; +import java.util.Vector; + +import com.cloud.dc.HostPodVO; +import com.cloud.utils.db.GenericDao; + +public interface HostPodDao extends GenericDao { + public List listByDataCenterId(long id); + + public HostPodVO findByName(String name, long dcId); + + public HashMap> getCurrentPodCidrSubnets(long zoneId, long podIdToSkip); + +} diff --git a/core/src/com/cloud/dc/dao/HostPodDaoImpl.java b/core/src/com/cloud/dc/dao/HostPodDaoImpl.java new file mode 100644 index 00000000000..03bd0d1e1a9 --- /dev/null +++ b/core/src/com/cloud/dc/dao/HostPodDaoImpl.java @@ -0,0 +1,289 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.dc.HostPodVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.net.NetUtils; + +@Local(value={HostPodDao.class}) +public class HostPodDaoImpl extends GenericDaoBase implements HostPodDao { + private static final Logger s_logger = Logger.getLogger(HostPodDaoImpl.class); + + protected SearchBuilder DataCenterAndNameSearch; + protected SearchBuilder DataCenterIdSearch; + + protected HostPodDaoImpl() { + DataCenterAndNameSearch = createSearchBuilder(); + DataCenterAndNameSearch.and("dc", DataCenterAndNameSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DataCenterAndNameSearch.and("name", DataCenterAndNameSearch.entity().getName(), SearchCriteria.Op.EQ); + DataCenterAndNameSearch.done(); + + DataCenterIdSearch = createSearchBuilder(); + DataCenterIdSearch.and("dcId", DataCenterIdSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DataCenterIdSearch.done(); + } + + public List listByDataCenterId(long id) { + SearchCriteria sc = DataCenterIdSearch.create(); + sc.setParameters("dcId", id); + + return listActiveBy(sc); + } + + public HostPodVO findByName(String name, long dcId) { + SearchCriteria sc = DataCenterAndNameSearch.create(); + sc.setParameters("dc", dcId); + sc.setParameters("name", name); + + return findOneActiveBy(sc); + } + + @Override + public HashMap> getCurrentPodCidrSubnets(long zoneId, long podIdToSkip) { + HashMap> currentPodCidrSubnets = new HashMap>(); + + String selectSql = "SELECT id, cidr_address, cidr_size FROM host_pod_ref WHERE data_center_id=" + zoneId; + Transaction txn = Transaction.currentTxn(); + try { + PreparedStatement stmt = txn.prepareAutoCloseStatement(selectSql); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + Long podId = rs.getLong("id"); + if (podId.longValue() == podIdToSkip) continue; + String cidrAddress = rs.getString("cidr_address"); + long cidrSize = rs.getLong("cidr_size"); + List cidrPair = new ArrayList(); + cidrPair.add(0, cidrAddress); + cidrPair.add(1, new Long(cidrSize)); + currentPodCidrSubnets.put(podId, cidrPair); + } + } catch (SQLException ex) { + s_logger.warn("DB exception " + ex.getMessage(), ex); + return null; + } + + return currentPodCidrSubnets; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); +/* + SearchBuilder builder = createSearchBuilder(); + builder.and("abc", builder.entity().getId(), SearchCriteria.Op.EQ); + builder.op(Op.AND, "def", builder.entity().getId(), SearchCriteria.Op.GT); + builder.or("fee", builder.entity().getId(), SearchCriteria.Op.LT); + builder.cp(); + builder.and("jjj", builder.entity().getId(), SearchCriteria.Op.EQ); + builder.done(); + SearchCriteria sc = builder.create(); + sc.setParameters("abc", 1); + sc.setParameters("def", 1); + sc.setParameters("fee", 1); + sc.setParameters("jjj", 1); + searchAll(sc, null, null, false); + try { + Transaction txn2 = Transaction.open("a1"); + txn2 = Transaction.open("a2"); + PreparedStatement pstmt2 = txn2.prepareAutoCloseStatement("SELECT id FROM host_pod_ref"); + pstmt2.executeQuery(); + txn2.close(); + txn2.setSavepoint(); + txn2.rollback(); + txn2.start(); + Transaction.open("a3"); + txn2.setSavepoint(); + txn2.start(); + Transaction.open("a4"); + Savepoint sp = txn2.setSavepoint(); + Transaction.open("a5"); + txn2.rollback(sp); + pstmt2 = txn2.prepareAutoCloseStatement("SELECT id FROM host_pod_ref"); + pstmt2.executeQuery(); + txn2.close(); + txn2.rollback(); + txn2.close(); + txn2.close(); + txn2.close(); + // This tests multiple starts and one single rollback rolls them all back. + txn2.open("a6"); + txn2.start(); + pstmt2 = txn2.prepareAutoCloseStatement("SELECT id FROM host_pod_ref"); + pstmt2.executeQuery(); + txn2.start(); + pstmt2 = txn2.prepareAutoCloseStatement("SELECT id FROM host_pod_ref"); + pstmt2.executeQuery(); + txn2.open("a7"); + txn2.start(); + pstmt2 = txn2.prepareAutoCloseStatement("SELECT id FROM host_pod_ref"); + pstmt2.executeQuery(); + txn2.rollback(); + txn2.close(); + txn2.close(); + + // This tests multiple starts and need multiple commits. + txn2.open("a8"); + txn2.start(); + pstmt2 = txn2.prepareAutoCloseStatement("SELECT id FROM host_pod_ref"); + pstmt2.executeQuery(); + txn2.start(); + pstmt2 = txn2.prepareAutoCloseStatement("SELECT id FROM host_pod_ref"); + pstmt2.executeQuery(); + txn2.open("a9"); + txn2.start(); + pstmt2 = txn2.prepareAutoCloseStatement("SELECT id FROM host_pod_ref"); + pstmt2.executeQuery(); + txn2.commit(); + txn2.close(); + txn2.commit(); + txn2.commit(); + txn2.close(); + + + } catch (Exception e) { + s_logger.warn("Exeception", e); + } + + Merovingian m = new Merovingian(Transaction.VMOPS_DB); + m.acquire("key1", 100); + m.acquire("key1", 100); + m.release("key1"); + m.acquire("key2", 100); + m.acquire("key3", 1000); + m.acquire("key1", 1000); + m.release("key3"); + m.release("key1"); + m.release("key2"); + m.release("key1"); + + + acquire(1l); + release(1l); + */ + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + ArrayList podIds = new ArrayList(); + PreparedStatement pstmt = txn.prepareAutoCloseStatement("SELECT id FROM host_pod_ref FOR UPDATE"); + ResultSet rs = pstmt.executeQuery(); + int i = 1; + while (rs.next()) { + podIds.add(rs.getLong(1)); + } + PreparedStatement alter = txn.prepareAutoCloseStatement("ALTER TABLE host_pod_ref ADD COLUMN cidr_address VARCHAR(15) NOT NULL"); + try { + int result = alter.executeUpdate(); + if (result == 0) { + txn.rollback(); + return true; + } + } catch (SQLException e) { + txn.rollback(); + + if (e.getMessage().contains("Duplicate column name")) { + s_logger.info("host_pod_ref table is already up to date"); + return true; + } + + // assume this is because it's already been updated. + s_logger.debug("Got this while updating", e); + + throw new ConfigurationException("Unable to update the host_pod_ref table "); + } + alter = txn.prepareStatement("ALTER TABLE host_pod_ref ADD COLUMN cidr_size bigint NOT NULL"); + try { + int result = alter.executeUpdate(); + if (result == 0) { + txn.rollback(); + throw new ConfigurationException("How can the first ALTER work but this doesn't?"); + } + } catch (SQLException e) { + s_logger.warn("Couldn't alter the table: ", e); + txn.rollback(); + throw new ConfigurationException("How can the first ALTER work but this doesn't? " + e.getMessage()); + } + + PreparedStatement netmask = txn.prepareAutoCloseStatement("SELECT value FROM configuration WHERE name='private.net.mask'"); + String privateNetmask; + try { + rs = netmask.executeQuery(); + if (!rs.next()) { + txn.rollback(); + throw new ConfigurationException("There's no private.netmask?"); + } + privateNetmask = rs.getString(1); + } catch (SQLException e) { + s_logger.warn("Couldn't get private.netmask due to ", e); + txn.rollback(); + throw new ConfigurationException("Unable to find the private.netmask"); + } + + for (Long podId : podIds) { + PreparedStatement ip = txn.prepareAutoCloseStatement("SELECT ip_address from op_dc_ip_address_alloc where pod_id=? LIMIT 0,1"); + ip.setLong(1, podId); + String addr = "192.168.1.1"; + try { + rs = ip.executeQuery(); + if (rs.next()) { + addr = rs.getString(1); + } else { + s_logger.debug("Default pod " + podId + " to 192.168.1.1 because it has no ip addresses allocated to it"); + } + } catch(SQLException e) { + s_logger.warn("Didn't work for " + podId + " due to " + e.getMessage(), e); + } + PreparedStatement update = txn.prepareAutoCloseStatement("UPDATE host_pod_ref set cidr_address=?, cidr_size=? WHERE id=?"); + update.setString(1, addr); + update.setLong(2, NetUtils.getCidrSize(privateNetmask)); + update.setLong(3, podId); + + try { + update.executeUpdate(); + } catch (SQLException e) { + s_logger.debug("Unable to update host_pod_ref table due to " + e.getMessage(), e); + } + } + + txn.commit(); + } catch (SQLException e) { + s_logger.error("Unable to upgrade the db due to " + e); + txn.rollback(); + throw new ConfigurationException("Unable to upgrade the db due to " + e); + } + + return true; + } +} diff --git a/core/src/com/cloud/dc/dao/PodVlanDaoImpl.java b/core/src/com/cloud/dc/dao/PodVlanDaoImpl.java new file mode 100755 index 00000000000..f29e7bd7070 --- /dev/null +++ b/core/src/com/cloud/dc/dao/PodVlanDaoImpl.java @@ -0,0 +1,138 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.dc.dao; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Date; +import java.util.List; + +import com.cloud.dc.PodVlanVO; +import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.exception.CloudRuntimeException; + +/** + * PodVlanDaoImpl maintains the one-to-many relationship between + * pod and the vlan that appears within its network. + */ +public class PodVlanDaoImpl extends GenericDaoBase implements GenericDao { + private final SearchBuilder FreeVlanSearch; + private final SearchBuilder VlanPodSearch; + private final SearchBuilder PodSearchAllocated; + + public List listAllocatedVnets(long podId) { + SearchCriteria sc = PodSearchAllocated.create(); + sc.setParameters("podId", podId); + return listActiveBy(sc); + } + + public void add(long podId, int start, int end) { + String insertVnet = "INSERT INTO `cloud`.`op_pod_vlan_alloc` (vlan, pod_id) VALUES ( ?, ?)"; + + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + PreparedStatement stmt = txn.prepareAutoCloseStatement(insertVnet); + for (int i = start; i < end; i++) { + stmt.setString(1, String.valueOf(i)); + stmt.setLong(2, podId); + stmt.addBatch(); + } + stmt.executeBatch(); + txn.commit(); + } catch (SQLException e) { + throw new CloudRuntimeException("Exception caught adding vnet ", e); + } + } + + public void delete(long podId) { + String deleteVnet = "DELETE FROM `cloud`.`op_pod_vlan_alloc` WHERE pod_id = ?"; + + Transaction txn = Transaction.currentTxn(); + try { + PreparedStatement stmt = txn.prepareAutoCloseStatement(deleteVnet); + stmt.setLong(1, podId); + stmt.executeUpdate(); + } catch (SQLException e) { + throw new CloudRuntimeException("Exception caught deleting vnet ", e); + } + } + + public PodVlanVO take(long podId, long accountId) { + SearchCriteria sc = FreeVlanSearch.create(); + sc.setParameters("podId", podId); + Date now = new Date(); + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + PodVlanVO vo = lock(sc, true); + if (vo == null) { + return null; + } + + vo.setTakenAt(now); + vo.setAccountId(accountId); + update(vo.getId(), vo); + txn.commit(); + return vo; + + } catch (Exception e) { + throw new CloudRuntimeException("Caught Exception ", e); + } + } + + public void release(String vlan, long podId, long accountId) { + SearchCriteria sc = VlanPodSearch.create(); + sc.setParameters("vlan", vlan); + sc.setParameters("podId", podId); + sc.setParameters("account", accountId); + + PodVlanVO vo = findOneBy(sc); + if (vo == null) { + return; + } + + vo.setTakenAt(null); + vo.setAccountId(null); + update(vo.getId(), vo); + } + + protected PodVlanDaoImpl() { + super(); + PodSearchAllocated = createSearchBuilder(); + PodSearchAllocated.and("podId", PodSearchAllocated.entity().getPodId(), SearchCriteria.Op.EQ); + PodSearchAllocated.and("allocated", PodSearchAllocated.entity().getTakenAt(), SearchCriteria.Op.NNULL); + PodSearchAllocated.done(); + + FreeVlanSearch = createSearchBuilder(); + FreeVlanSearch.and("podId", FreeVlanSearch.entity().getPodId(), SearchCriteria.Op.EQ); + FreeVlanSearch.and("taken", FreeVlanSearch.entity().getTakenAt(), SearchCriteria.Op.NULL); + FreeVlanSearch.done(); + + VlanPodSearch = createSearchBuilder(); + VlanPodSearch.and("vlan", VlanPodSearch.entity().getVlan(), SearchCriteria.Op.EQ); + VlanPodSearch.and("podId", VlanPodSearch.entity().getPodId(), SearchCriteria.Op.EQ); + VlanPodSearch.and("taken", VlanPodSearch.entity().getTakenAt(), SearchCriteria.Op.NNULL); + VlanPodSearch.and("account", VlanPodSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + VlanPodSearch.done(); + } +} diff --git a/core/src/com/cloud/dc/dao/PodVlanMapDao.java b/core/src/com/cloud/dc/dao/PodVlanMapDao.java new file mode 100644 index 00000000000..bf18b401050 --- /dev/null +++ b/core/src/com/cloud/dc/dao/PodVlanMapDao.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc.dao; + +import java.util.List; + +import com.cloud.dc.PodVlanMapVO; +import com.cloud.utils.db.GenericDao; + +public interface PodVlanMapDao extends GenericDao { + + public List listPodVlanMapsByPod(long podId); + public List listPodVlanMapsByVlan(long vlanDbId); + public PodVlanMapVO findPodVlanMap(long podId, long vlanDbId); + +} diff --git a/core/src/com/cloud/dc/dao/PodVlanMapDaoImpl.java b/core/src/com/cloud/dc/dao/PodVlanMapDaoImpl.java new file mode 100644 index 00000000000..617a084a64b --- /dev/null +++ b/core/src/com/cloud/dc/dao/PodVlanMapDaoImpl.java @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.dc.PodVlanMapVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={PodVlanMapDao.class}) +public class PodVlanMapDaoImpl extends GenericDaoBase implements PodVlanMapDao { + + protected SearchBuilder PodSearch; + protected SearchBuilder VlanSearch; + protected SearchBuilder PodVlanSearch; + + @Override + public List listPodVlanMapsByPod(long podId) { + SearchCriteria sc = PodSearch.create(); + sc.setParameters("podId", podId); + return listBy(sc); + } + + @Override + public List listPodVlanMapsByVlan(long vlanDbId) { + SearchCriteria sc = VlanSearch.create(); + sc.setParameters("vlanDbId", vlanDbId); + return listBy(sc); + } + + @Override + public PodVlanMapVO findPodVlanMap(long podId, long vlanDbId) { + SearchCriteria sc = PodVlanSearch.create(); + sc.setParameters("podId", podId); + sc.setParameters("vlanDbId", vlanDbId); + return findOneBy(sc); + } + + public PodVlanMapDaoImpl() { + PodSearch = createSearchBuilder(); + PodSearch.and("podId", PodSearch.entity().getPodId(), SearchCriteria.Op.EQ); + PodSearch.done(); + + VlanSearch = createSearchBuilder(); + VlanSearch.and("vlanDbId", VlanSearch.entity().getVlanDbId(), SearchCriteria.Op.EQ); + VlanSearch.done(); + + PodVlanSearch = createSearchBuilder(); + PodVlanSearch.and("podId", PodVlanSearch.entity().getPodId(), SearchCriteria.Op.EQ); + PodVlanSearch.and("vlanDbId", PodVlanSearch.entity().getVlanDbId(), SearchCriteria.Op.EQ); + PodVlanSearch.done(); + } + +} diff --git a/core/src/com/cloud/dc/dao/VlanDao.java b/core/src/com/cloud/dc/dao/VlanDao.java new file mode 100644 index 00000000000..b70df875ed9 --- /dev/null +++ b/core/src/com/cloud/dc/dao/VlanDao.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc.dao; + +import java.util.List; + +import com.cloud.dc.Vlan; +import com.cloud.dc.VlanVO; +import com.cloud.dc.Vlan.VlanType; +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; + +public interface VlanDao extends GenericDao { + + public VlanVO findByZoneAndVlanId(long zoneId, String vlanId); + + public List findByZone(long zoneId); + + public List listByZoneAndType(long zoneId, Vlan.VlanType vlanType); + + public List listVlansForPod(long podId); + + public List listVlansForPodByType(long podId, Vlan.VlanType vlanType); + + public void addToPod(long podId, long vlanDbId); + + public Pair assignIpAddress(long zoneId, long accountId, long domainId, VlanType vlanType, boolean sourceNat); + + List listVlansForAccountByType(Long zoneId, long accountId, VlanType vlanType); + + public Pair assignPodDirectAttachIpAddress(long zoneId, long podId, long accountId, long domainId); + + boolean zoneHasDirectAttachUntaggedVlans(long zoneId); + +} diff --git a/core/src/com/cloud/dc/dao/VlanDaoImpl.java b/core/src/com/cloud/dc/dao/VlanDaoImpl.java new file mode 100644 index 00000000000..76e78b7b6f7 --- /dev/null +++ b/core/src/com/cloud/dc/dao/VlanDaoImpl.java @@ -0,0 +1,258 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.dc.dao; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import com.cloud.dc.AccountVlanMapVO; +import com.cloud.dc.PodVlanMapVO; +import com.cloud.dc.Vlan; +import com.cloud.dc.VlanVO; +import com.cloud.dc.Vlan.VlanType; +import com.cloud.network.dao.IPAddressDao; +import com.cloud.utils.Pair; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={VlanDao.class}) +public class VlanDaoImpl extends GenericDaoBase implements VlanDao { + + protected SearchBuilder ZoneVlanIdSearch; + protected SearchBuilder ZoneSearch; + protected SearchBuilder ZoneTypeSearch; + protected SearchBuilder ZoneTypeAllPodsSearch; + protected SearchBuilder ZoneTypePodSearch; + + + protected PodVlanMapDaoImpl _podVlanMapDao = new PodVlanMapDaoImpl(); + protected AccountVlanMapDao _accountVlanMapDao = new AccountVlanMapDaoImpl(); + protected IPAddressDao _ipAddressDao = null; + + @Override + public VlanVO findByZoneAndVlanId(long zoneId, String vlanId) { + SearchCriteria sc = ZoneVlanIdSearch.create(); + sc.setParameters("zoneId", zoneId); + sc.setParameters("vlanId", vlanId); + return findOneActiveBy(sc); + } + + @Override + public List findByZone(long zoneId) { + SearchCriteria sc = ZoneSearch.create(); + sc.setParameters("zoneId", zoneId); + return listBy(sc); + } + + public VlanDaoImpl() { + ZoneVlanIdSearch = createSearchBuilder(); + ZoneVlanIdSearch.and("zoneId", ZoneVlanIdSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + ZoneVlanIdSearch.and("vlanId", ZoneVlanIdSearch.entity().getVlanId(), SearchCriteria.Op.EQ); + ZoneVlanIdSearch.done(); + + ZoneSearch = createSearchBuilder(); + ZoneSearch.and("zoneId", ZoneSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + ZoneSearch.done(); + + ZoneTypeSearch = createSearchBuilder(); + ZoneTypeSearch.and("zoneId", ZoneTypeSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + ZoneTypeSearch.and("vlanType", ZoneTypeSearch.entity().getVlanType(), SearchCriteria.Op.EQ); + ZoneTypeSearch.done(); + } + + @Override + public List listByZoneAndType(long zoneId, VlanType vlanType) { + SearchCriteria sc = ZoneTypeSearch.create(); + sc.setParameters("zoneId", zoneId); + sc.setParameters("vlanType", vlanType); + return listBy(sc); + } + + @Override + public List listVlansForPod(long podId) { + //FIXME: use a join statement to improve the performance (should be minor since we expect only one or two + List vlanMaps = _podVlanMapDao.listPodVlanMapsByPod(podId); + List result = new ArrayList(); + for (PodVlanMapVO pvmvo: vlanMaps) { + result.add(findById(pvmvo.getVlanDbId())); + } + return result; + } + + @Override + public List listVlansForPodByType(long podId, VlanType vlanType) { + //FIXME: use a join statement to improve the performance (should be minor since we expect only one or two) + List vlanMaps = _podVlanMapDao.listPodVlanMapsByPod(podId); + List result = new ArrayList(); + for (PodVlanMapVO pvmvo: vlanMaps) { + VlanVO vlan =findById(pvmvo.getVlanDbId()); + if (vlan.getVlanType() == vlanType) { + result.add(vlan); + } + } + return result; + } + + @Override + public List listVlansForAccountByType(Long zoneId, long accountId, VlanType vlanType) { + //FIXME: use a join statement to improve the performance (should be minor since we expect only one or two) + List vlanMaps = _accountVlanMapDao.listAccountVlanMapsByAccount(accountId); + List result = new ArrayList(); + for (AccountVlanMapVO acvmvo: vlanMaps) { + VlanVO vlan =findById(acvmvo.getVlanDbId()); + if (vlan.getVlanType() == vlanType && (zoneId == null || vlan.getDataCenterId() == zoneId)) { + result.add(vlan); + } + } + return result; + } + + @Override + public void addToPod(long podId, long vlanDbId) { + PodVlanMapVO pvmvo = new PodVlanMapVO(podId, vlanDbId); + _podVlanMapDao.persist(pvmvo); + + } + + @Override + public boolean configure(String name, Map params) + throws ConfigurationException { + boolean result = super.configure(name, params); + if (result) { + final ComponentLocator locator = ComponentLocator.getCurrentLocator(); + _ipAddressDao = locator.getDao(IPAddressDao.class); + if (_ipAddressDao == null) { + throw new ConfigurationException("Unable to get " + IPAddressDao.class.getName()); + } + } + ZoneTypeAllPodsSearch = createSearchBuilder(); + ZoneTypeAllPodsSearch.and("zoneId", ZoneTypeAllPodsSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + ZoneTypeAllPodsSearch.and("vlanType", ZoneTypeAllPodsSearch.entity().getVlanType(), SearchCriteria.Op.EQ); + + SearchBuilder PodVlanSearch = _podVlanMapDao.createSearchBuilder(); + PodVlanSearch.and("podId", PodVlanSearch.entity().getPodId(), SearchCriteria.Op.NNULL); + ZoneTypeAllPodsSearch.join("vlan", PodVlanSearch, PodVlanSearch.entity().getVlanDbId(), ZoneTypeAllPodsSearch.entity().getId()); + + ZoneTypeAllPodsSearch.done(); + PodVlanSearch.done(); + + ZoneTypePodSearch = createSearchBuilder(); + ZoneTypePodSearch.and("zoneId", ZoneTypePodSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + ZoneTypePodSearch.and("vlanType", ZoneTypePodSearch.entity().getVlanType(), SearchCriteria.Op.EQ); + + SearchBuilder PodVlanSearch2 = _podVlanMapDao.createSearchBuilder(); + PodVlanSearch2.and("podId", PodVlanSearch2.entity().getPodId(), SearchCriteria.Op.EQ); + ZoneTypePodSearch.join("vlan", PodVlanSearch2, PodVlanSearch2.entity().getVlanDbId(), ZoneTypePodSearch.entity().getId()); + PodVlanSearch2.done(); + ZoneTypePodSearch.done(); + + return result; + } + + private VlanVO findNextVlan(long zoneId, Vlan.VlanType vlanType) { + List allVlans = listByZoneAndType(zoneId, vlanType); + List emptyVlans = new ArrayList(); + List fullVlans = new ArrayList(); + + // Try to find a VLAN that is partially allocated + for (VlanVO vlan : allVlans) { + long vlanDbId = vlan.getId(); + + int countOfAllocatedIps = _ipAddressDao.countIPs(zoneId, vlanDbId, true); + int countOfAllIps = _ipAddressDao.countIPs(zoneId, vlanDbId, false); + + if ((countOfAllocatedIps > 0) && (countOfAllocatedIps < countOfAllIps)) { + return vlan; + } else if (countOfAllocatedIps == 0) { + emptyVlans.add(vlan); + } else if (countOfAllocatedIps == countOfAllIps) { + fullVlans.add(vlan); + } + } + + if (emptyVlans.isEmpty()) { + return null; + } + + // Try to find an empty VLAN with the same tag/subnet as a VLAN that is full + for (VlanVO fullVlan : fullVlans) { + for (VlanVO emptyVlan : emptyVlans) { + if (fullVlan.getVlanId().equals(emptyVlan.getVlanId()) && + fullVlan.getVlanGateway().equals(emptyVlan.getVlanGateway()) && + fullVlan.getVlanNetmask().equals(emptyVlan.getVlanNetmask())) { + return emptyVlan; + } + } + } + + // Return a random empty VLAN + return emptyVlans.get(0); + } + + @Override + public Pair assignIpAddress(long zoneId, long accountId, long domainId, VlanType vlanType, boolean sourceNat) { + VlanVO vlan = findNextVlan(zoneId, vlanType); + if (vlan == null) { + return null; + } + String ipAddress = _ipAddressDao.assignIpAddress(accountId, domainId, vlan.getId(), sourceNat); + if (ipAddress == null) { + return null; + } + return new Pair(ipAddress, vlan); + } + + @Override + public boolean zoneHasDirectAttachUntaggedVlans(long zoneId) { + SearchCriteria sc = ZoneTypeAllPodsSearch.create(); + sc.setParameters("zoneId", zoneId); + sc.setParameters("vlanType", VlanType.DirectAttached); + + return listBy(sc).size() > 0; + } + + + @Override + public Pair assignPodDirectAttachIpAddress(long zoneId, + long podId, long accountId, long domainId) { + SearchCriteria sc = ZoneTypePodSearch.create(); + sc.setParameters("zoneId", zoneId); + sc.setParameters("vlanType", VlanType.DirectAttached); + sc.setJoinParameters("vlan", "podId", podId); + + VlanVO vlan = findOneBy(sc); + if (vlan == null) { + return null; + } + + String ipAddress = _ipAddressDao.assignIpAddress(accountId, domainId, vlan.getId(), false); + if (ipAddress == null) { + return null; + } + return new Pair(ipAddress, vlan); + + } + +} diff --git a/core/src/com/cloud/domain/DomainVO.java b/core/src/com/cloud/domain/DomainVO.java new file mode 100644 index 00000000000..90ee26b5976 --- /dev/null +++ b/core/src/com/cloud/domain/DomainVO.java @@ -0,0 +1,143 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.domain; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="domain") +public class DomainVO { + public static final long ROOT_DOMAIN = 1L; + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="parent") + private Long parent = null; + + @Column(name="name") + private String name = null; + + @Column(name="owner") + private Long owner = null; + + @Column(name="path") + private String path = null; + + @Column(name="level") + private Integer level = null; + + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + @Column(name="child_count") + private int childCount = 0; + + @Column(name="next_child_seq") + private long nextChildSeq = 1L; + + public DomainVO() {} + + public DomainVO(String name, Long owner, Long parentId) { + this.parent = parentId; + this.name = name; + this.owner = owner; + this.path =""; + this.level = 0; + } + + public Long getId() { + return id; + } + + public Long getParent() { + return parent; + } + + public void setParent(Long parent) { + if(parent == null) { + this.parent = DomainVO.ROOT_DOMAIN; + } else { + if(parent.longValue() <= DomainVO.ROOT_DOMAIN) + this.parent = DomainVO.ROOT_DOMAIN; + else + this.parent = parent; + } + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getOwner() { + return owner; + } + + public Date getRemoved() { + return removed; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Integer getLevel() { + return level; + } + + public void setLevel(Integer level) { + this.level = level; + } + + public int getChildCount() { + return childCount; + } + + public void setChildCount(int count) { + childCount = count; + } + + public long getNextChildSeq() { + return nextChildSeq; + } + + public void setNextChildSeq(long seq) { + nextChildSeq = seq; + } +} + diff --git a/core/src/com/cloud/domain/dao/DomainDao.java b/core/src/com/cloud/domain/dao/DomainDao.java new file mode 100644 index 00000000000..7bbff1624dd --- /dev/null +++ b/core/src/com/cloud/domain/dao/DomainDao.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.domain.dao; + +import com.cloud.domain.DomainVO; +import com.cloud.utils.db.GenericDao; + +public interface DomainDao extends GenericDao { + public void update(Long id, String domainName); + public DomainVO create(DomainVO domain); + public DomainVO findDomainByPath(String domainPath); + public boolean isChildDomain(Long parentId, Long childId); +} diff --git a/core/src/com/cloud/domain/dao/DomainDaoImpl.java b/core/src/com/cloud/domain/dao/DomainDaoImpl.java new file mode 100644 index 00000000000..dc2633cdd6f --- /dev/null +++ b/core/src/com/cloud/domain/dao/DomainDaoImpl.java @@ -0,0 +1,222 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.domain.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.domain.DomainVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GlobalLock; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value={DomainDao.class}) +public class DomainDaoImpl extends GenericDaoBase implements DomainDao { + private static final Logger s_logger = Logger.getLogger(DomainDaoImpl.class); + + protected SearchBuilder DomainNameLikeSearch; + protected SearchBuilder ParentDomainNameLikeSearch; + protected SearchBuilder DomainPairSearch; + + public DomainDaoImpl () { + DomainNameLikeSearch = createSearchBuilder(); + DomainNameLikeSearch.and("name", DomainNameLikeSearch.entity().getName(), SearchCriteria.Op.LIKE); + DomainNameLikeSearch.done(); + + ParentDomainNameLikeSearch = createSearchBuilder(); + ParentDomainNameLikeSearch.and("name", ParentDomainNameLikeSearch.entity().getName(), SearchCriteria.Op.LIKE); + ParentDomainNameLikeSearch.and("parent", ParentDomainNameLikeSearch.entity().getName(), SearchCriteria.Op.EQ); + ParentDomainNameLikeSearch.done(); + + DomainPairSearch = createSearchBuilder(); + DomainPairSearch.and("id", DomainPairSearch.entity().getId(), SearchCriteria.Op.IN); + DomainPairSearch.done(); + } + + public void update(Long id, String domainName) { + DomainVO ub = createForUpdate(); + ub.setName(domainName); + update(id, ub); + } + + private static String allocPath(DomainVO parentDomain, String name) { + String parentPath = parentDomain.getPath(); + return parentPath + name + "/"; + } + + @Override + public synchronized DomainVO create(DomainVO domain) { + // make sure domain name is valid + String domainName = domain.getName(); + if (domainName != null) { + if (domainName.contains("/")) { + throw new IllegalArgumentException("Domain name contains one or more invalid characters. Please enter a name without '/' characters."); + } + } else { + throw new IllegalArgumentException("Domain name is null. Please specify a valid domain name."); + } + + long parent = DomainVO.ROOT_DOMAIN; + if(domain.getParent() != null && domain.getParent().longValue() >= DomainVO.ROOT_DOMAIN) { + parent = domain.getParent().longValue(); + } + + DomainVO parentDomain = findById(parent); + if(parentDomain == null) { + s_logger.error("Unable to load parent domain: " + parent); + return null; + } + + GlobalLock lock = GlobalLock.getInternLock("lock.domain." + parent); + if(!lock.lock(3600)) { + // wait up to 1 hour, if it comes up to here, something is wrong + s_logger.error("Unable to lock parent domain: " + parent); + return null; + } + + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + + domain.setPath(allocPath(parentDomain, domain.getName())); + domain.setLevel(parentDomain.getLevel() + 1); + + parentDomain.setNextChildSeq(parentDomain.getNextChildSeq() + 1); // FIXME: remove sequence number? + parentDomain.setChildCount(parentDomain.getChildCount() + 1); + persist(domain); + update(parentDomain.getId(), parentDomain); + + txn.commit(); + return domain; + } catch(Exception e) { + s_logger.error("Unable to create domain due to " + e.getMessage(), e); + txn.rollback(); + return null; + } finally { + lock.unlock(); + } + } + + @Override + public boolean remove(Long id) { + // check for any active users / domains assigned to the given domain id and don't remove the domain if there are any + if (id != null && id.longValue() == DomainVO.ROOT_DOMAIN) { + s_logger.error("Can not remove domain " + id + " as it is ROOT domain"); + return false; + } + + DomainVO domain = findById(id); + if(domain == null) { + s_logger.error("Unable to remove domain as domain " + id + " no longer exists"); + return false; + } + + if(domain.getParent() == null) { + s_logger.error("Invalid domain " + id + ", orphan?"); + return false; + } + + DomainVO parentDomain = findById(domain.getParent()); + if(parentDomain == null) { + s_logger.error("Unable to load parent domain: " + domain.getParent()); + return false; + } + + GlobalLock lock = GlobalLock.getInternLock("lock.domain." + domain.getParent()); + if(!lock.lock(Integer.MAX_VALUE)) { + s_logger.error("Unable to lock parent domain: " + domain.getParent()); + return false; + } + + String sql = "SELECT * from account where domain_id = " + id + " and removed is null"; + String sql1 = "SELECT * from domain where parent = " + id + " and removed is null"; + + boolean success = false; + Transaction txn = Transaction.currentTxn(); + try { + PreparedStatement stmt = txn.prepareAutoCloseStatement(sql); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + return false; + } + stmt = txn.prepareAutoCloseStatement(sql1); + rs = stmt.executeQuery(); + if (rs.next()) { + return false; + } + + txn.start(); + parentDomain.setChildCount(parentDomain.getChildCount() - 1); + update(parentDomain.getId(), parentDomain); + success = super.remove(id); + txn.commit(); + } catch (SQLException ex) { + success = false; + s_logger.error("error removing domain: " + id, ex); + txn.rollback(); + } finally { + lock.unlock(); + } + return success; + } + + @Override + public DomainVO findDomainByPath(String domainPath) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("path", SearchCriteria.Op.EQ, domainPath); + return findOneActiveBy(sc); + } + + @Override + public boolean isChildDomain(Long parentId, Long childId) { + if ((parentId == null) || (childId == null)) { + return false; + } + + if (parentId.equals(childId)) { + return true; + } + + boolean result = false; + SearchCriteria sc = DomainPairSearch.create(); + sc.setParameters("id", parentId, childId); + + List domainPair = listActiveBy(sc); + + if ((domainPair != null) && (domainPair.size() == 2)) { + DomainVO d1 = domainPair.get(0); + DomainVO d2 = domainPair.get(1); + + if (d1.getId().equals(parentId)) { + result = d2.getPath().startsWith(d1.getPath()); + } else { + result = d1.getPath().startsWith(d2.getPath()); + } + } + return result; + } +} diff --git a/core/src/com/cloud/event/EventState.java b/core/src/com/cloud/event/EventState.java new file mode 100644 index 00000000000..e6c32b135e5 --- /dev/null +++ b/core/src/com/cloud/event/EventState.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.event; + +public enum EventState { + Scheduled, + Started, + Completed; +} \ No newline at end of file diff --git a/core/src/com/cloud/event/EventTypes.java b/core/src/com/cloud/event/EventTypes.java new file mode 100644 index 00000000000..4ba66ee4762 --- /dev/null +++ b/core/src/com/cloud/event/EventTypes.java @@ -0,0 +1,138 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.event; + +public class EventTypes { + // VM Events + public static final String EVENT_VM_CREATE = "VM.CREATE"; + public static final String EVENT_VM_DESTROY = "VM.DESTROY"; + public static final String EVENT_VM_START = "VM.START"; + public static final String EVENT_VM_STOP = "VM.STOP"; + public static final String EVENT_VM_REBOOT = "VM.REBOOT"; + public static final String EVENT_VM_DISABLE_HA = "VM.DISABLEHA"; + public static final String EVENT_VM_ENABLE_HA = "VM.ENABLEHA"; + public static final String EVENT_VM_UPGRADE = "VM.UPGRADE"; + public static final String EVENT_VM_RESETPASSWORD = "VM.RESETPASSWORD"; + + // Domain Router + public static final String EVENT_ROUTER_CREATE = "ROUTER.CREATE"; + public static final String EVENT_ROUTER_DESTROY = "ROUTER.DESTROY"; + public static final String EVENT_ROUTER_START = "ROUTER.START"; + public static final String EVENT_ROUTER_STOP = "ROUTER.STOP"; + public static final String EVENT_ROUTER_REBOOT = "ROUTER.REBOOT"; + public static final String EVENT_ROUTER_HA = "ROUTER.HA"; + + // Console proxy + public static final String EVENT_PROXY_CREATE = "PROXY.CREATE"; + public static final String EVENT_PROXY_DESTROY = "PROXY.DESTROY"; + public static final String EVENT_PROXY_START = "PROXY.START"; + public static final String EVENT_PROXY_STOP = "PROXY.STOP"; + public static final String EVENT_PROXY_REBOOT = "PROXY.REBOOT"; + public static final String EVENT_PROXY_HA = "PROXY.HA"; + + // VNC Console Events + public static final String EVENT_VNC_CONNECT = "VNC.CONNECT"; + public static final String EVENT_VNC_DISCONNECT = "VNC.DISCONNECT"; + + // Network Events + public static final String EVENT_NET_IP_ASSIGN = "NET.IPASSIGN"; + public static final String EVENT_NET_IP_RELEASE = "NET.IPRELEASE"; + public static final String EVENT_NET_RULE_ADD = "NET.RULEADD"; + public static final String EVENT_NET_RULE_DELETE = "NET.RULEDELETE"; + public static final String EVENT_NET_RULE_MODIFY = "NET.RULEMODIFY"; + + // Security Groups + public static final String EVENT_PORT_FORWARDING_SERVICE_APPLY = "PF.SERVICE.APPLY"; + public static final String EVENT_PORT_FORWARDING_SERVICE_DELETE = "PF.SERVICE.DELETE"; + public static final String EVENT_PORT_FORWARDING_SERVICE_REMOVE = "PF.SERVICE.REMOVE"; + public static final String EVENT_LOAD_BALANCER_CREATE = "LB.CREATE"; + public static final String EVENT_LOAD_BALANCER_DELETE = "LB.DELETE"; + + // UserVO Events + public static final String EVENT_USER_LOGIN = "USER.LOGIN"; + public static final String EVENT_USER_LOGOUT = "USER.LOGOUT"; + public static final String EVENT_USER_CREATE = "USER.CREATE"; + public static final String EVENT_USER_DELETE = "USER.DELETE"; + public static final String EVENT_USER_UPDATE = "USER.UPDATE"; + + //Template Events + public static final String EVENT_TEMPLATE_CREATE = "TEMPLATE.CREATE"; + public static final String EVENT_TEMPLATE_DELETE = "TEMPLATE.DELETE"; + public static final String EVENT_TEMPLATE_UPDATE = "TEMPLATE.UPDATE"; + public static final String EVENT_TEMPLATE_COPY = "TEMPLATE.COPY"; + public static final String EVENT_TEMPLATE_DOWNLOAD_START = "TEMPLATE.DOWNLOAD.START"; + public static final String EVENT_TEMPLATE_DOWNLOAD_SUCCESS = "TEMPLATE.DOWNLOAD.SUCCESS"; + public static final String EVENT_TEMPLATE_DOWNLOAD_FAILED = "TEMPLATE.DOWNLOAD.FAILED"; + + // Volume Events + public static final String EVENT_VOLUME_CREATE = "VOLUME.CREATE"; + public static final String EVENT_VOLUME_DELETE = "VOLUME.DELETE"; + public static final String EVENT_VOLUME_ATTACH = "VOLUME.ATTACH"; + public static final String EVENT_VOLUME_DETACH = "VOLUME.DETACH"; + + // Domains + public static final String EVENT_DOMAIN_CREATE = "DOMAIN.CREATE"; + public static final String EVENT_DOMAIN_DELETE = "DOMAIN.DELETE"; + public static final String EVENT_DOMAIN_UPDATE = "DOMAIN.UPDATE"; + + // Snapshots + public static final String EVENT_SNAPSHOT_CREATE = "SNAPSHOT.CREATE"; + public static final String EVENT_SNAPSHOT_DELETE = "SNAPSHOT.DELETE"; + public static final String EVENT_SNAPSHOT_POLICY_CREATE = "SNAPSHOTPOLICY.CREATE"; + public static final String EVENT_SNAPSHOT_POLICY_UPDATE = "SNAPSHOTPOLICY.UPDATE"; + public static final String EVENT_SNAPSHOT_POLICY_DELETE = "SNAPSHOTPOLICY.DELETE"; + + // ISO + public static final String EVENT_ISO_CREATE = "ISO.CREATE"; + public static final String EVENT_ISO_DELETE = "ISO.DELETE"; + public static final String EVENT_ISO_COPY = "ISO.COPY"; + public static final String EVENT_ISO_ATTACH = "ISO.ATTACH"; + public static final String EVENT_ISO_DETACH = "ISO.DETACH"; + + //SSVM + public static final String EVENT_SSVM_CREATE = "SSVM.CREATE"; + public static final String EVENT_SSVM_DESTROY = "SSVM.DESTROY"; + public static final String EVENT_SSVM_START = "SSVM.START"; + public static final String EVENT_SSVM_STOP = "SSVM.STOP"; + public static final String EVENT_SSVM_REBOOT = "SSVM.REBOOT"; + public static final String EVENT_SSVM_HA = "SSVM.HA"; + + // Service Offerings + public static final String EVENT_SERVICE_OFFERING_CREATE = "SERVICE.OFFERING.CREATE"; + public static final String EVENT_SERVICE_OFFERING_EDIT = "SERVICE.OFFERING.EDIT"; + public static final String EVENT_SERVICE_OFFERING_DELETE = "SERVICE.OFFERING.DELETE"; + + // Pods + public static final String EVENT_POD_CREATE = "POD.CREATE"; + public static final String EVENT_POD_EDIT = "POD.EDIT"; + public static final String EVENT_POD_DELETE = "POD.DELETE"; + + // Zones + public static final String EVENT_ZONE_CREATE = "ZONE.CREATE"; + public static final String EVENT_ZONE_EDIT = "ZONE.EDIT"; + public static final String EVENT_ZONE_DELETE = "ZONE.DELETE"; + + // VLANs/IP ranges + public static final String EVENT_VLAN_IP_RANGE_CREATE = "VLAN.IP.RANGE.CREATE"; + public static final String EVENT_VLAN_IP_RANGE_DELETE = "VLAN.IP.RANGE.DELETE"; + + // Configuration Table + public static final String EVENT_CONFIGURATION_VALUE_EDIT = "CONFIGURATION.VALUE.EDIT"; + +} diff --git a/core/src/com/cloud/event/EventVO.java b/core/src/com/cloud/event/EventVO.java new file mode 100644 index 00000000000..76919cbaebb --- /dev/null +++ b/core/src/com/cloud/event/EventVO.java @@ -0,0 +1,176 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.event; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.SecondaryTable; +import javax.persistence.Table; +import javax.persistence.Transient; +import javax.persistence.PrimaryKeyJoinColumn; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="event") +@SecondaryTable(name="account", + pkJoinColumns={@PrimaryKeyJoinColumn(name="account_id", referencedColumnName="id")}) +public class EventVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = (long)-1; + + @Column(name="type") + private String type; + + @Enumerated(value=EnumType.STRING) + @Column(name="state") + private EventState state = EventState.Completed; + + @Column(name="description") + private String description; + + @Column(name=GenericDao.CREATED_COLUMN) + private Date createDate; + + @Column(name="user_id") + private long userId; + + @Column(name="account_id") + private long accountId; + + @Column(name="domain_id", table="account", insertable=false, updatable=false) + private long domainId; + + @Column(name="account_name", table="account", insertable=false, updatable=false) + private String accountName; + + @Column(name="removed", table="account", insertable=false, updatable=false) + private Date removed; + + @Column(name="level") + private String level = LEVEL_INFO; + + @Column(name="start_id") + private long startId; + + @Column(name="parameters") + private String parameters; + + @Transient + private int totalSize; + + public static final String LEVEL_INFO = "INFO"; + public static final String LEVEL_WARN = "WARN"; + public static final String LEVEL_ERROR = "ERROR"; + + public EventVO() { + } + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public String getType() { + return type; + } + public void setType(String type) { + this.type = type; + } + public EventState getState() { + return state; + } + + public void setState(EventState state) { + this.state = state; + } + + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + public Date getCreateDate() { + return createDate; + } + public void setCreatedDate(Date createdDate) { + createDate = createdDate; + } + public long getUserId() { + return userId; + } + public void setUserId(long userId) { + this.userId = userId; + } + public long getAccountId() { + return accountId; + } + public void setAccountId(long accountId) { + this.accountId = accountId; + } + public long getDomainId() { + return domainId; + } + public void setDomainId(long domainId) { + this.domainId = domainId; + } + public String getAccountName() { + return accountName; + } + public void setAccountName(String accountName) { + this.accountName = accountName; + } + public int getTotalSize() { + return totalSize; + } + public void setTotalSize(int totalSize) { + this.totalSize = totalSize; + } + public String getLevel() { + return level; + } + public void setLevel(String level) { + this.level = level; + } + public long getStartId() { + return startId; + } + + public void setStartId(long startId) { + this.startId = startId; + } + + public String getParameters() { + return parameters; + } + public void setParameters(String parameters) { + this.parameters = parameters; + } +} diff --git a/core/src/com/cloud/event/dao/EventDao.java b/core/src/com/cloud/event/dao/EventDao.java new file mode 100644 index 00000000000..58390bb58ff --- /dev/null +++ b/core/src/com/cloud/event/dao/EventDao.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.event.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.event.EventVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.SearchCriteria; + +public interface EventDao extends GenericDao { + public List searchAllEvents(SearchCriteria sc, Filter filter); + + public List listOlderEvents(Date oldTime); + + List listStartedEvents(Date minTime, Date maxTime); + + EventVO findCompletedEvent(long startId); +} diff --git a/core/src/com/cloud/event/dao/EventDaoImpl.java b/core/src/com/cloud/event/dao/EventDaoImpl.java new file mode 100644 index 00000000000..45b34d5c3e1 --- /dev/null +++ b/core/src/com/cloud/event/dao/EventDaoImpl.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.event.dao; + +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.event.EventState; +import com.cloud.event.EventVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={EventDao.class}) +public class EventDaoImpl extends GenericDaoBase implements EventDao { + public static final Logger s_logger = Logger.getLogger(EventDaoImpl.class.getName()); + protected final SearchBuilder StartedEventsSearch; + protected final SearchBuilder CompletedEventSearch; + + public EventDaoImpl () { + StartedEventsSearch = createSearchBuilder(); + StartedEventsSearch.and("state",StartedEventsSearch.entity().getState(),SearchCriteria.Op.NEQ); + StartedEventsSearch.and("startId", StartedEventsSearch.entity().getStartId(), SearchCriteria.Op.EQ); + StartedEventsSearch.and("createDate", StartedEventsSearch.entity().getCreateDate(), SearchCriteria.Op.BETWEEN); + StartedEventsSearch.done(); + + CompletedEventSearch = createSearchBuilder(); + CompletedEventSearch.and("state",CompletedEventSearch.entity().getState(),SearchCriteria.Op.EQ); + CompletedEventSearch.and("startId", CompletedEventSearch.entity().getStartId(), SearchCriteria.Op.EQ); + CompletedEventSearch.done(); + } + + @Override + public List searchAllEvents(SearchCriteria sc, Filter filter) { + return listBy(sc, filter); + } + + @Override + public List listOlderEvents(Date oldTime) { + if (oldTime == null) return null; + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("createDate", SearchCriteria.Op.LT, oldTime); + return listBy(sc, null); + + } + + @Override + public List listStartedEvents(Date minTime, Date maxTime) { + if (minTime == null || maxTime == null) return null; + SearchCriteria sc = StartedEventsSearch.create(); + sc.setParameters("state", EventState.Completed); + sc.setParameters("startId", 0); + sc.setParameters("createDate", minTime, maxTime); + return listBy(sc, null); + } + + @Override + public EventVO findCompletedEvent(long startId) { + SearchCriteria sc = CompletedEventSearch.create(); + sc.setParameters("state", EventState.Completed); + sc.setParameters("startId", startId); + return findOneBy(sc); + } +} diff --git a/core/src/com/cloud/exception/AgentControlChannelException.java b/core/src/com/cloud/exception/AgentControlChannelException.java new file mode 100644 index 00000000000..b7254c0e540 --- /dev/null +++ b/core/src/com/cloud/exception/AgentControlChannelException.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.exception; + +public class AgentControlChannelException extends Exception { + private static final long serialVersionUID = -310647782960500466L; + + public AgentControlChannelException(String msg) { + super(msg); + } +} diff --git a/core/src/com/cloud/exception/OperationTimedoutException.java b/core/src/com/cloud/exception/OperationTimedoutException.java new file mode 100644 index 00000000000..e3d18f43407 --- /dev/null +++ b/core/src/com/cloud/exception/OperationTimedoutException.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.agent.api.Command; +import com.cloud.utils.SerialVersionUID; + +/** + * This exception is thrown when the operation couldn't complete due to a + * wait timeout. + */ +public class OperationTimedoutException extends Exception { + private static final long serialVersionUID = SerialVersionUID.OperationTimedoutException; + + long _agentId; + long _seqId; + int _time; + Command[] _cmds; + boolean _isActive; + + public OperationTimedoutException(Command[] cmds, long agentId, long seqId, int time, boolean isActive) { + super("Commands " + seqId + " to Host " + agentId + " timed out after " + time); + _agentId = agentId; + _seqId = seqId; + _time = time; + _cmds = cmds; + _isActive = isActive; + } + + public long getAgentId() { + return _agentId; + } + + public long getSequenceId() { + return _seqId; + } + + public int getWaitTime() { + return _time; + } + + public Command[] getCommands() { + return _cmds; + } + + public boolean isActive() { + return _isActive; + } +} diff --git a/core/src/com/cloud/exception/UnsupportedVersionException.java b/core/src/com/cloud/exception/UnsupportedVersionException.java new file mode 100755 index 00000000000..4f6adb51739 --- /dev/null +++ b/core/src/com/cloud/exception/UnsupportedVersionException.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.exception; + +import com.cloud.utils.SerialVersionUID; + +/** + * Version of the protocol is no longer supported. + */ +public class UnsupportedVersionException extends Exception { + + private static final long serialVersionUID = SerialVersionUID.UnsupportedVersionException; + + public static final String UnknownVersion = "unknown.version"; + public static final String IncompatibleVersion = "incompatible.version"; + + String _reason; + + public UnsupportedVersionException(String message, String reason) { + super(message); + _reason = reason; + } + + public String getReason() { + return _reason; + } +} diff --git a/core/src/com/cloud/ha/FenceBuilder.java b/core/src/com/cloud/ha/FenceBuilder.java new file mode 100644 index 00000000000..2af745b630a --- /dev/null +++ b/core/src/com/cloud/ha/FenceBuilder.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.ha; + +import com.cloud.host.HostVO; +import com.cloud.utils.component.Adapter; +import com.cloud.vm.VMInstanceVO; + +public interface FenceBuilder extends Adapter { + /** + * Fence off the vm. + * + * @param vm vm + * @param host host where the vm was running on. + * @return true if it was done. null if it was not responsible for this vm. + */ + public Boolean fenceOff(VMInstanceVO vm, HostVO host); +} diff --git a/core/src/com/cloud/ha/HighAvailabilityManager.java b/core/src/com/cloud/ha/HighAvailabilityManager.java new file mode 100644 index 00000000000..6daf321cb4c --- /dev/null +++ b/core/src/com/cloud/ha/HighAvailabilityManager.java @@ -0,0 +1,115 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.ha; + +import java.util.List; + +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.utils.component.Manager; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; + +/** + * HighAvailabilityManager checks to make sure the VMs are running fine. + */ +public interface HighAvailabilityManager extends Manager { + + enum Step { + Scheduled, + Investigating, + Fencing, + Stopping, + Restarting, + Preparing, + Migrating, + Checking, + Cancelled, + Done, + Error, + } + + /** + * Investigate why a host has disconnected and migrate the VMs on it + * if necessary. + * + * @param host - the host that has disconnected. + */ + Status investigate(long hostId); + + /** + * Restart a vm that has gone away due to various reasons. Whether a + * VM is restarted depends on various reasons. + * 1. Is the VM really dead. This method will try to find out. + * 2. Is the VM HA enabled? If not, the VM is simply stopped. + * + * All VMs that enter HA mode is not allowed to be operated on until it + * has been determined that the VM is dead. + * + * @param vm the vm that has gone away. + * @param investigate must be investigated before we do anything with this vm. + */ + void scheduleRestart(final VMInstanceVO vm, boolean investigate); + + void cancelDestroy(VMInstanceVO vm, Long hostId); + + void scheduleDestroy(VMInstanceVO vm, long hostId); + + /** + * Schedule restarts for all vms running on the host. + * @param host host. + */ + void scheduleRestartForVmsOnHost(final HostVO host); + + /** + * Register a handler to take care of a VM. If a handler for a certain + * type of VM is not handled, then it is not part of the HA process. + * + * @param type virtual machine type. + * @param handler handler that can handle starting and stopping the machine. + */ + void registerHandler(final VirtualMachine.Type type, final VirtualMachineManager handler); + + /** + * Unregisters a handler. Not likely called but here for completeness. + * @param type virtual machine type to unregister. + */ + void unregisterHandler(final VirtualMachine.Type type); + + /** + * Schedule the vm for migration. + * + * @param vm + * @return true if schedule worked. + */ + boolean scheduleMigration(final VMInstanceVO vm); + + List findTakenMigrationWork(); + + /** + * Stops a VM. + * + * @param vm virtual machine to stop. + * @param host host the virtual machine is on. + * @param verifyHost make sure it is the same host as the schedule time. + */ + void scheduleStop(final VMInstanceVO vm, long hostId, boolean verifyHost); + + void cancelScheduledMigrations(HostVO host); +} diff --git a/core/src/com/cloud/ha/Investigator.java b/core/src/com/cloud/ha/Investigator.java new file mode 100644 index 00000000000..50c960e2b49 --- /dev/null +++ b/core/src/com/cloud/ha/Investigator.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.ha; + +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.utils.component.Adapter; +import com.cloud.vm.VMInstanceVO; + +public interface Investigator extends Adapter { + /** + * Returns if the vm is still alive. + * + * @param vm to work on. + * @return null if not sure. true if sure it is alive. false if sure it is not. + */ + public Boolean isVmAlive(VMInstanceVO vm, HostVO host); + + public Status isAgentAlive(HostVO agent); +} diff --git a/core/src/com/cloud/ha/WorkVO.java b/core/src/com/cloud/ha/WorkVO.java new file mode 100644 index 00000000000..d7ac9622925 --- /dev/null +++ b/core/src/com/cloud/ha/WorkVO.java @@ -0,0 +1,204 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.ha; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.ha.HighAvailabilityManager.Step; +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.State; +import com.cloud.vm.VirtualMachine; + +@Entity +@Table(name="op_ha_work") +public class WorkVO { + public enum WorkType { + Migration, + Stop, + CheckStop, + Destroy, + HA; + } + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="instance_id", updatable=false, nullable=false) + private long instanceId; // vm_instance id + + @Column(name="mgmt_server_id", nullable=true) + private Long serverId; + + @Column(name=GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name="state", nullable=false) + @Enumerated(value=EnumType.STRING) + private State previousState; + + @Column(name="host_id", nullable=false) + private long hostId; + + @Column(name="taken", nullable=true) + @Temporal(value=TemporalType.TIMESTAMP) + private Date dateTaken; + + @Column(name="time_to_try", nullable=true) + private long timeToTry; + + @Column(name="type", updatable = false, nullable=false) + @Enumerated(value=EnumType.STRING) + private WorkType workType; + + @Column(name="updated") + private long updateTime; + + @Column(name="step", nullable = false) + @Enumerated(value=EnumType.STRING) + private HighAvailabilityManager.Step step; + + @Column(name="vm_type", updatable = false, nullable=false) + @Enumerated(value=EnumType.STRING) + private VirtualMachine.Type type; + + @Column(name="tried") + int timesTried; + + protected WorkVO() { + } + + public Long getId() { + return id; + } + + public long getInstanceId() { + return instanceId; + } + + public WorkType getWorkType() { + return workType; + } + + public void setStep(final HighAvailabilityManager.Step step) { + this.step = step; + } + + public Long getServerId() { + return serverId; + } + + public VirtualMachine.Type getType() { + return type; + } + + public void setServerId(final Long serverId) { + this.serverId = serverId; + } + + public Date getCreated() { + return created; + } + + public void setHostId(final long hostId) { + this.hostId = hostId; + } + + public HighAvailabilityManager.Step getStep() { + return step; + } + + public State getPreviousState() { + return previousState; + } + + public Date getDateTaken() { + return dateTaken; + } + + public long getHostId() { + return hostId; + } + + public void setDateTaken(final Date taken) { + this.dateTaken = taken; + } + + public void setTimesTried(final int time) { + timesTried = time; + } + + public boolean canScheduleNew(final long interval) { + return (timeToTry + interval) < (System.currentTimeMillis() >> 10); + } + + public int getTimesTried() { + return timesTried; + } + + public long getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(long time) { + updateTime = time; + } + + public long getTimeToTry() { + return timeToTry; + } + + public void setTimeToTry(final long timeToTry) { + this.timeToTry = timeToTry; + } + + public void setPreviousState(State state) { + this.previousState = state; + } + + public WorkVO(final long instanceId, final VirtualMachine.Type type, final WorkType workType, final Step step, final long hostId, final State previousState, final int timesTried, final long updated) { + this.workType = workType; + this.type = type; + this.instanceId = instanceId; + this.serverId = null; + this.hostId = hostId; + this.previousState = previousState; + this.dateTaken = null; + this.timesTried = timesTried; + this.step = step; + this.timeToTry = System.currentTimeMillis() >> 10; + this.updateTime = updated; + } + + @Override + public String toString() { + return new StringBuilder("[HA-Work:id=").append(id).append(":type=").append(workType.toString()).append(":vm=").append(instanceId).append(":state=").append(previousState.toString()).append("]").toString(); + } +} diff --git a/core/src/com/cloud/ha/dao/HighAvailabilityDao.java b/core/src/com/cloud/ha/dao/HighAvailabilityDao.java new file mode 100644 index 00000000000..67adc2311e1 --- /dev/null +++ b/core/src/com/cloud/ha/dao/HighAvailabilityDao.java @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.ha.dao; + +import java.util.List; + +import com.cloud.ha.WorkVO; +import com.cloud.ha.WorkVO.WorkType; +import com.cloud.utils.db.GenericDao; + +public interface HighAvailabilityDao extends GenericDao { + + /** + * Takes an available HA work item. + * + * @param serverId server that is taking this. + * @return WorkVO if there's one to work on; null if none. + */ + WorkVO take(long serverId); + + /** + * Finds all the work items related to this instance. + * + * @param instanceId + * @return list of WorkVO or empty list. + */ + List findPreviousHA(long instanceId); + + boolean delete(long instanceId, WorkType type); + + /** + * Finds all the work items that were successful and is now ready to be purged. + * + * @param time that the work item must be successful before. + * @return list of WorkVO or empty list. + */ + void cleanup(long time); + + void deleteMigrationWorkItems(final long hostId, final WorkType type, final long serverId); + + List findTakenWorkItems(WorkType type); + + /** + * finds out if a work item has been scheduled for this work type but has not been taken yet. + * + * @param instanceId vm instance id + * @param type type of work scheduled for it. + * @return true if it has been scheduled and false if it hasn't. + */ + boolean hasBeenScheduled(long instanceId, WorkType type); +} diff --git a/core/src/com/cloud/ha/dao/HighAvailabilityDaoImpl.java b/core/src/com/cloud/ha/dao/HighAvailabilityDaoImpl.java new file mode 100644 index 00000000000..9d5e5c85773 --- /dev/null +++ b/core/src/com/cloud/ha/dao/HighAvailabilityDaoImpl.java @@ -0,0 +1,174 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.ha.dao; + +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.ha.HighAvailabilityManager; +import com.cloud.ha.WorkVO; +import com.cloud.ha.HighAvailabilityManager.Step; +import com.cloud.ha.WorkVO.WorkType; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.exception.CloudRuntimeException; + +@Local(value={HighAvailabilityDao.class}) +public class HighAvailabilityDaoImpl extends GenericDaoBase implements HighAvailabilityDao { + private static final Logger s_logger = Logger.getLogger(HighAvailabilityDaoImpl.class); + + private final SearchBuilder TBASearch; + private final SearchBuilder PreviousInstanceSearch; + private final SearchBuilder UntakenMigrationSearch; + private final SearchBuilder CleanupSearch; + private final SearchBuilder PreviousWorkSearch; + private final SearchBuilder TakenWorkSearch; + + protected HighAvailabilityDaoImpl() { + super(); + + CleanupSearch = createSearchBuilder(); + CleanupSearch.and("time", CleanupSearch.entity().getTimeToTry(), SearchCriteria.Op.LTEQ); + CleanupSearch.and("step", CleanupSearch.entity().getStep(), SearchCriteria.Op.IN); + CleanupSearch.done(); + + TBASearch = createSearchBuilder(); + TBASearch.and("server", TBASearch.entity().getServerId(), SearchCriteria.Op.NULL); + TBASearch.and("taken", TBASearch.entity().getDateTaken(), SearchCriteria.Op.NULL); + TBASearch.and("time", TBASearch.entity().getTimeToTry(), SearchCriteria.Op.LTEQ); + TBASearch.done(); + + PreviousInstanceSearch = createSearchBuilder(); + PreviousInstanceSearch.and("instance", PreviousInstanceSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + PreviousInstanceSearch.done(); + + UntakenMigrationSearch = createSearchBuilder(); + UntakenMigrationSearch.and("host", UntakenMigrationSearch.entity().getHostId(), SearchCriteria.Op.EQ); + UntakenMigrationSearch.and("type", UntakenMigrationSearch.entity().getWorkType(), SearchCriteria.Op.EQ); + UntakenMigrationSearch.and("server", UntakenMigrationSearch.entity().getServerId(), SearchCriteria.Op.NULL); + UntakenMigrationSearch.and("taken", UntakenMigrationSearch.entity().getDateTaken(), SearchCriteria.Op.NULL); + UntakenMigrationSearch.done(); + + TakenWorkSearch = createSearchBuilder(); + TakenWorkSearch.and("type", TakenWorkSearch.entity().getWorkType(), SearchCriteria.Op.EQ); + TakenWorkSearch.and("server", TakenWorkSearch.entity().getServerId(), SearchCriteria.Op.NNULL); + TakenWorkSearch.and("taken", TakenWorkSearch.entity().getDateTaken(), SearchCriteria.Op.NNULL); + TakenWorkSearch.and("step", TakenWorkSearch.entity().getStep(), SearchCriteria.Op.NIN); + TakenWorkSearch.done(); + + PreviousWorkSearch = createSearchBuilder(); + PreviousWorkSearch.and("instance", PreviousWorkSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + PreviousWorkSearch.and("type", PreviousWorkSearch.entity().getWorkType(), SearchCriteria.Op.EQ); + PreviousWorkSearch.and("taken", PreviousWorkSearch.entity().getDateTaken(), SearchCriteria.Op.NULL); + PreviousWorkSearch.done(); + } + + @Override + public WorkVO take(final long serverId) { + final Transaction txn = Transaction.currentTxn(); + try { + final SearchCriteria sc = TBASearch.create(); + sc.setParameters("time", System.currentTimeMillis() >> 10); + + final Filter filter = new Filter(WorkVO.class, null, true, 0l, 1l); + + txn.start(); + final List vos = lock(sc, filter, true); + if (vos.size() == 0) { + txn.commit(); + return null; + } + + final WorkVO work = vos.get(0); + work.setServerId(serverId); + work.setDateTaken(new Date()); + + update(work.getId(), work); + + txn.commit(); + + return work; + + } catch (final Throwable e) { + throw new CloudRuntimeException("Unable to execute take", e); + } + } + + @Override + public List findPreviousHA(final long instanceId) { + final SearchCriteria sc = PreviousInstanceSearch.create(); + sc.setParameters("instance", instanceId); + return listBy(sc); + } + + @Override + public void cleanup(final long time) { + final SearchCriteria sc = CleanupSearch.create(); + sc.setParameters("time", time); + sc.setParameters("step", HighAvailabilityManager.Step.Done, HighAvailabilityManager.Step.Cancelled); + delete(sc); + } + + @Override + public void deleteMigrationWorkItems(final long hostId, final WorkType type, final long serverId) { + final SearchCriteria sc = UntakenMigrationSearch.create(); + sc.setParameters("host", hostId); + sc.setParameters("type", type.toString()); + + WorkVO work = createForUpdate(); + Date date = new Date(); + work.setDateTaken(date); + work.setServerId(serverId); + work.setStep(HighAvailabilityManager.Step.Cancelled); + + update(work, sc); + } + + @Override + public List findTakenWorkItems(WorkType type) { + SearchCriteria sc = TakenWorkSearch.create(); + sc.setParameters("type", type); + sc.setParameters("step", Step.Done, Step.Cancelled, Step.Error); + + return listActiveBy(sc); + } + + + @Override + public boolean delete(long instanceId, WorkType type) { + SearchCriteria sc = PreviousWorkSearch.create(); + sc.setParameters("instance", instanceId); + sc.setParameters("type", type); + return delete(sc) > 0; + } + + @Override + public boolean hasBeenScheduled(long instanceId, WorkType type) { + SearchCriteria sc = PreviousWorkSearch.create(); + sc.setParameters("instance", instanceId); + sc.setParameters("type", type); + return listActiveBy(sc, null).size() > 0; + } +} \ No newline at end of file diff --git a/core/src/com/cloud/host/DetailVO.java b/core/src/com/cloud/host/DetailVO.java new file mode 100644 index 00000000000..021590a3c90 --- /dev/null +++ b/core/src/com/cloud/host/DetailVO.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="host_details") +public class DetailVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private long id; + + @Column(name="host_id") + private long hostId; + + @Column(name="name") + private String name; + + @Column(name="value") + private String value; + + protected DetailVO() { + } + + public DetailVO(long hostId, String name, String value) { + this.hostId = hostId; + this.name = name; + this.value = value; + } + + public long getHostId() { + return hostId; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public long getId() { + return id; + } +} diff --git a/core/src/com/cloud/host/Host.java b/core/src/com/cloud/host/Host.java new file mode 100755 index 00000000000..a8ec5301d32 --- /dev/null +++ b/core/src/com/cloud/host/Host.java @@ -0,0 +1,168 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host; + +import java.util.Date; + +import com.cloud.hypervisor.Hypervisor; + + +/** + * Host represents one particular host server. + */ +public interface Host { + public enum Type { + Storage(false), + Routing(false), + SecondaryStorage(false), + ConsoleProxy(true); + + boolean _virtual; + private Type(boolean virtual) { + _virtual = virtual; + } + + public boolean isVirtual() { + return _virtual; + } + + public static String[] toStrings(Host.Type... types) { + String[] strs = new String[types.length]; + for (int i = 0; i < types.length; i++) { + strs[i] = types[i].toString(); + } + return strs; + } + } + + /** + * @return id of the host. + */ + Long getId(); + + /** + * @return name of the machine. + */ + String getName(); + + /** + * @return the type of host. + */ + Type getType(); + + /** + * @return the date the host first registered + */ + Date getCreated(); + + /** + * @return current state of this machine. + */ + Status getStatus(); + + /** + * @return the ip address of the host. + */ + String getPrivateIpAddress(); + + /** + * @return the ip address of the host attached to the storage network. + */ + String getStorageIpAddress(); + + /** + * @return the mac address of the host. + */ + String getGuid(); + + /** + * @return total amount of memory. + */ + Long getTotalMemory(); + + /** + * @return # of cores in a machine. Note two cpus with two cores each returns 4. + */ + Integer getCpus(); + + /** + * @return speed of each cpu in mhz. + */ + Long getSpeed(); + + /** + * @return the proxy port that is being listened at the agent host + */ + Integer getProxyPort(); + + /** + * @return the pod. + */ + Long getPodId(); + + /** + * @return availability zone. + */ + long getDataCenterId(); + + /** + * @return parent path. only used for storage server. + */ + String getParent(); + + /** + * @return storage ip address. + */ + String getStorageIpAddressDeux(); + + /** + * @return type of hypervisor + */ + Hypervisor.Type getHypervisorType(); + + /** + * @return disconnection date + */ + Date getDisconnectedOn(); + /** + * @return version + */ + public String getVersion(); + /* + * @return total size + */ + public long getTotalSize(); + /* + * @return capabilities + */ + public String getCapabilities(); + /* + * @return last pinged time + */ + public long getLastPinged(); + /* + * @return management server id + */ + public Long getManagementServerId(); + /* + *@return removal date + */ + public Date getRemoved(); + + public Long getClusterId(); +} diff --git a/core/src/com/cloud/host/HostEnvironment.java b/core/src/com/cloud/host/HostEnvironment.java new file mode 100644 index 00000000000..59cc78de463 --- /dev/null +++ b/core/src/com/cloud/host/HostEnvironment.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2010 VMOps, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host; + + +/** + * HostEnvironment is a in memory object that describes + * the networking environment of the host. The + * information may be incomplete and particularly for + * the first host in the server is definitely incomplete. + * This allows the server to fill out and compare + * the environment. + */ +public class HostEnvironment { + + public String managementIpAddress; + public String managementNetmask; + public String managementGateway; + public String managementVlan; + + public String[] neighborHosts; + + public String storageIpAddress; + public String storageNetwork; + public String storageGateway; + public String storageVlan; + public String secondaryStroageIpAddress; + + public String storage2IpAddress; + public String storage2Network; + public String storage2Gateway; + public String storage2Vlan; + public String secondaryStorageIpAddress2; + + public String[] neighborStorages; + public String[] neighborStorages2; + + public String publicIpAddress; + public String publicNetmask; + public String publicGateway; + public String publicVlan; +} diff --git a/core/src/com/cloud/host/HostInfo.java b/core/src/com/cloud/host/HostInfo.java new file mode 100644 index 00000000000..5dee3776c9b --- /dev/null +++ b/core/src/com/cloud/host/HostInfo.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host; + +/** + * @author chiradeep + * + */ +public final class HostInfo { + public static final String HYPERVISOR_VERSION = "Hypervisor.Version"; //tricky since KVM has userspace version and kernel version + public static final String HOST_OS = "Host.OS"; //Fedora, XenServer, Ubuntu, etc + public static final String HOST_OS_VERSION = "Host.OS.Version"; //12, 5.5, 9.10, etc + public static final String HOST_OS_KERNEL_VERSION = "Host.OS.Kernel.Version"; //linux-2.6.31 etc + +} diff --git a/core/src/com/cloud/host/HostStats.java b/core/src/com/cloud/host/HostStats.java new file mode 100755 index 00000000000..128754bf6ca --- /dev/null +++ b/core/src/com/cloud/host/HostStats.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host; + +/** + * @author ahuang + * + */ +public interface HostStats { + + //host related stats + public double getAverageLoad(); + public double getCpuUtilization(); + public double getNetworkWriteKBs(); + public double getTotalMemoryKBs(); + public double getFreeMemoryKBs(); + public double getXapiMemoryUsageKBs(); + public double getNetworkReadKBs(); + public String getEntityType(); + public double getUsedMemory(); + public int getNumCpus(); + public HostStats getHostStats(); +} diff --git a/core/src/com/cloud/host/HostVO.java b/core/src/com/cloud/host/HostVO.java new file mode 100644 index 00000000000..c6c05672bac --- /dev/null +++ b/core/src/com/cloud/host/HostVO.java @@ -0,0 +1,628 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host; + +import java.util.Date; +import java.util.Map; + +import javax.persistence.Column; +import javax.persistence.DiscriminatorColumn; +import javax.persistence.DiscriminatorType; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.Transient; + +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="host") +@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS) +@DiscriminatorColumn(name="type", discriminatorType=DiscriminatorType.STRING, length=32) +public class HostVO implements Host { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="disconnected") + @Temporal(value=TemporalType.TIMESTAMP) + private Date disconnectedOn; + + @Column(name="name", nullable=false) + private String name = null; + + /** + * Note: There is no setter for status because it has to be set in the dao code. + */ + @Column(name="status", nullable=false) + private Status status = null; + + @Column(name="type", updatable = true, nullable=false) + @Enumerated(value=EnumType.STRING) + private Type type; + + @Column(name="private_ip_address", nullable=false) + private String privateIpAddress; + + @Column(name="private_mac_address", nullable=false) + private String privateMacAddress; + + @Column(name="private_netmask", nullable=false) + private String privateNetmask; + + @Column(name="public_netmask") + private String publicNetmask; + + @Column(name="public_ip_address") + private String publicIpAddress; + + @Column(name="public_mac_address") + private String publicMacAddress; + + @Column(name="storage_ip_address") + private String storageIpAddress; + + @Column(name="cluster_id") + private Long clusterId; + + @Column(name="storage_netmask") + private String storageNetmask; + + @Column(name="storage_mac_address") + private String storageMacAddress; + + @Column(name="storage_ip_address_2") + private String storageIpAddressDeux; + + @Column(name="storage_netmask_2") + private String storageNetmaskDeux; + + @Column(name="storage_mac_address_2") + private String storageMacAddressDeux; + + @Column(name="hypervisor_type", updatable = true, nullable=false) + @Enumerated(value=EnumType.STRING) + private Hypervisor.Type hypervisorType; + + @Column(name="proxy_port") + private Integer proxyPort; + + @Column(name="resource") + private String resource; + + @Column(name="fs_type") + private StoragePoolType fsType; + + @Column(name="available") + private boolean available = true; + + @Column(name="setup") + private boolean setup = false; + + // This is a delayed load value. If the value is null, + // then this field has not been loaded yet. + // Call host dao to load it. + @Transient + Map details; + + @Override + public String getStorageIpAddressDeux() { + return storageIpAddressDeux; + } + + public void setStorageIpAddressDeux(String deuxStorageIpAddress) { + this.storageIpAddressDeux = deuxStorageIpAddress; + } + + public String getStorageNetmaskDeux() { + return storageNetmaskDeux; + } + + public Long getClusterId() { + return clusterId; + } + + public void setClusterId(Long clusterId) { + this.clusterId = clusterId; + } + + public void setStorageNetmaskDeux(String deuxStorageNetmask) { + this.storageNetmaskDeux = deuxStorageNetmask; + } + + public String getStorageMacAddressDeux() { + return storageMacAddressDeux; + } + + public void setStorageMacAddressDeux(String duexStorageMacAddress) { + this.storageMacAddressDeux = duexStorageMacAddress; + } + + public String getPrivateMacAddress() { + return privateMacAddress; + } + + public void setPrivateMacAddress(String privateMacAddress) { + this.privateMacAddress = privateMacAddress; + } + + public boolean isAvailable() { + return available; + } + + public void setAvailable(boolean available) { + this.available = available; + } + + public String getPrivateNetmask() { + return privateNetmask; + } + + public void setPrivateNetmask(String privateNetmask) { + this.privateNetmask = privateNetmask; + } + + public String getPublicNetmask() { + return publicNetmask; + } + + public void setPublicNetmask(String publicNetmask) { + this.publicNetmask = publicNetmask; + } + + public String getPublicIpAddress() { + return publicIpAddress; + } + + public void setPublicIpAddress(String publicIpAddress) { + this.publicIpAddress = publicIpAddress; + } + + public String getPublicMacAddress() { + return publicMacAddress; + } + + public void setPublicMacAddress(String publicMacAddress) { + this.publicMacAddress = publicMacAddress; + } + + @Override + public String getStorageIpAddress() { + return storageIpAddress; + } + + public void setStorageIpAddress(String storageIpAddress) { + this.storageIpAddress = storageIpAddress; + } + + public String getStorageNetmask() { + return storageNetmask; + } + + public void setStorageNetmask(String storageNetmask) { + this.storageNetmask = storageNetmask; + } + + public String getStorageMacAddress() { + return storageMacAddress; + } + + public boolean isSetup() { + return setup; + } + + public void setSetup(boolean setup) { + this.setup = setup; + } + + public void setStorageMacAddress(String storageMacAddress) { + this.storageMacAddress = storageMacAddress; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Map getDetails() { + return details; + } + + public String getDetail(String name) { + assert (details != null) : "Did you forget to load the details?"; + + return details != null ? details.get(name) : null; + } + + public void setDetail(String name, String value) { + assert (details != null) : "Did you forget to load the details?"; + + details.put(name, value); + } + + public void setDetails(Map details) { + this.details = details; + } + + @Column(name="data_center_id", nullable=false) + private long dataCenterId; + + @Column(name="pod_id") + private Long podId; + + @Column(name="cpus") + private Integer cpus; + + @Column(name="url") + private String storageUrl; + + @Column(name="speed") + private Long speed; + + @Column(name="ram") + private long totalMemory; + + @Column(name="parent", nullable=false) + private String parent; + + @Column(name="guid", updatable=true, nullable=false) + private String guid; + + @Column(name="capabilities") + private String caps; + + @Column(name="total_size") + private Long totalSize; + + @Column(name="last_ping") + private long lastPinged; + + @Column(name="mgmt_server_id") + private Long managementServerId; + + @Column(name="dom0_memory") + private long dom0MinMemory; + + @Column(name="version") + private String version; + + @Column(name=GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + public HostVO(String guid) { + this.guid = guid; + this.status = Status.Up; + this.totalMemory = 0; + this.dom0MinMemory = 0; + } + + protected HostVO() { + } + + public HostVO(Long id, + String name, + Type type, + String privateIpAddress, + String privateNetmask, + String privateMacAddress, + String publicIpAddress, + String publicNetmask, + String publicMacAddress, + String storageIpAddress, + String storageNetmask, + String storageMacAddress, + String deuxStorageIpAddress, + String duxStorageNetmask, + String deuxStorageMacAddress, + String guid, + Status status, + String version, + String iqn, + Date disconnectedOn, + long dcId, + Long podId, + long serverId, + long ping, + String parent, + long totalSize, + StoragePoolType fsType) { + this(id, name, type, privateIpAddress, privateNetmask, privateMacAddress, publicIpAddress, publicNetmask, publicMacAddress, storageIpAddress, storageNetmask, storageMacAddress, guid, status, version, iqn, disconnectedOn, dcId, podId, serverId, ping, null, null, null, 0, null); + this.parent = parent; + this.totalSize = totalSize; + this.fsType = fsType; + } + + public HostVO(Long id, + String name, + Type type, + String privateIpAddress, + String privateNetmask, + String privateMacAddress, + String publicIpAddress, + String publicNetmask, + String publicMacAddress, + String storageIpAddress, + String storageNetmask, + String storageMacAddress, + String guid, + Status status, + String version, + String url, + Date disconnectedOn, + long dcId, + Long podId, + long serverId, + long ping, + Integer cpus, + Long speed, + Long totalMemory, + long dom0MinMemory, + String caps) { + this.id = id; + this.name = name; + this.status = status; + this.type = type; + this.privateIpAddress = privateIpAddress; + this.privateNetmask = privateNetmask; + this.privateMacAddress = privateMacAddress; + this.publicIpAddress = publicIpAddress; + this.publicNetmask = publicNetmask; + this.publicMacAddress = publicMacAddress; + this.storageIpAddress = storageIpAddress; + this.storageNetmask = storageNetmask; + this.storageMacAddress = storageMacAddress; + this.dataCenterId = dcId; + this.podId = podId; + this.cpus = cpus; + this.version = version; + this.speed = speed; + this.totalMemory = totalMemory != null ? totalMemory : 0; + this.guid = guid; + this.parent = null; + this.totalSize = null; + this.fsType = null; + this.managementServerId = serverId; + this.lastPinged = ping; + this.caps = caps; + this.disconnectedOn = disconnectedOn; + this.dom0MinMemory = dom0MinMemory; + this.storageUrl = url; + } + + public void setPodId(Long podId) { + + this.podId = podId; + } + + public void setDataCenterId(long dcId) { + this.dataCenterId = dcId; + } + + public void setVersion(String version) { + this.version = version; + } + + public void setStorageUrl(String url) { + this.storageUrl = url; + } + + public void setDisconnectedOn(Date disconnectedOn) { + this.disconnectedOn = disconnectedOn; + } + + public String getStorageUrl() { + return storageUrl; + } + + public void setName(String name) { + this.name = name; + } + + public void setPrivateIpAddress(String ipAddress) { + this.privateIpAddress = ipAddress; + } + + public void setCpus(Integer cpus) { + this.cpus = cpus; + } + + public void setSpeed(Long speed) { + this.speed = speed; + } + + public void setTotalMemory(long totalMemory) { + this.totalMemory = totalMemory; + } + + public void setParent(String parent) { + this.parent = parent; + } + + public void setCaps(String caps) { + this.caps = caps; + } + + public void setTotalSize(Long totalSize) { + this.totalSize = totalSize; + } + + public void setLastPinged(long lastPinged) { + this.lastPinged = lastPinged; + } + + public void setManagementServerId(Long managementServerId) { + this.managementServerId = managementServerId; + } + + public long getLastPinged() { + return lastPinged; + } + + @Override + public String getParent() { + return parent; + } + + public long getTotalSize() { + return totalSize; + } + + public String getCapabilities() { + return caps; + } + + @Override + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + public String getVersion() { + return version; + } + + public void setType(Type type) { + this.type = type; + } + + public Long getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public Status getStatus() { + return status; + } + + @Override + public long getDataCenterId() { + return dataCenterId; + } + + @Override + public Long getPodId() { + return podId; + } + + public Long getManagementServerId() { + return managementServerId; + } + + public Date getDisconnectedOn() { + return disconnectedOn; + } + + @Override + public String getPrivateIpAddress() { + return privateIpAddress; + } + + @Override + public String getGuid() { + return guid; + } + + public void setGuid(String guid) { + this.guid = guid; + } + + public Integer getCpus() { + return cpus; + } + + @Override + public Long getSpeed() { + return speed; + } + + @Override + public Long getTotalMemory() { + return totalMemory; + } + + @Override + public Integer getProxyPort() { + return proxyPort; + } + + public void setProxyPort(Integer port) { + proxyPort = port; + } + + public StoragePoolType getFsType() { + return fsType; + } + + @Override + public Type getType() { + return type; + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : -1; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof HostVO) { + return ((HostVO)obj).getId() == this.getId(); + } else { + return false; + } + } + + @Override + public String toString() { + return new StringBuilder(type.toString()).append("-").append(Long.toString(id)).append("-").append(name).toString(); + } + + public void setHypervisorType(Hypervisor.Type hypervisorType) { + this.hypervisorType = hypervisorType; + } + + @Override + public Hypervisor.Type getHypervisorType() { + return hypervisorType; + } +} diff --git a/core/src/com/cloud/host/InsufficientServerCapacityException.java b/core/src/com/cloud/host/InsufficientServerCapacityException.java new file mode 100755 index 00000000000..83dc611243a --- /dev/null +++ b/core/src/com/cloud/host/InsufficientServerCapacityException.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host; + +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.utils.SerialVersionUID; +import com.cloud.vm.VirtualMachine; + +/** + * This exception is thrown when a server cannot be found to host the + * virtual machine. The type gives the type of virtual machine we are + * trying to start. + */ +public class InsufficientServerCapacityException extends InsufficientCapacityException { + + private static final long serialVersionUID = SerialVersionUID.InsufficientServerCapacityException; + + VirtualMachine.Type type; + public InsufficientServerCapacityException(VirtualMachine.Type type, String msg) { + super(msg); + this.type = type; + } + + VirtualMachine.Type getType() { + return type; + } +} diff --git a/core/src/com/cloud/host/Status.java b/core/src/com/cloud/host/Status.java new file mode 100644 index 00000000000..5adf487445e --- /dev/null +++ b/core/src/com/cloud/host/Status.java @@ -0,0 +1,186 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host; + +import java.util.List; +import java.util.Set; + +import com.cloud.utils.fsm.StateMachine; + +public enum Status { + Connecting(true, false, false), + Up(true, false, false), + Down(true, true, true), + Disconnected(true, true, true), + Updating(true, true, false), + PrepareForMaintenance(false, false, false), + ErrorInMaintenance(false, false, false), + Maintenance(false, false, false), + Alert(true, true, true), + Removed(true, false, true); + + private final boolean updateManagementServer; + private final boolean checkManagementServer; + private final boolean lostConnection; + + private Status(boolean updateConnection, boolean checkManagementServer, boolean lostConnection) { + this.updateManagementServer = updateConnection; + this.checkManagementServer = checkManagementServer; + this.lostConnection = lostConnection; + } + + public boolean updateManagementServer() { + return updateManagementServer; + } + + public boolean checkManagementServer() { + return checkManagementServer; + } + + public boolean lostConnection() { + return lostConnection; + } + + public enum Event { + AgentConnected(false, "Agent connected"), + PingTimeout(false, "Agent is behind on ping"), + UpdateNeeded(false, "UpdateRequested"), + ShutdownRequested(false, "Shutdown requested by the agent"), + AgentDisconnected(false, "Agent disconnected"), + ResetRequested(true, "Reset is requested by the user"), + HostDown(false, "Host is found to be down by the investigator"), + PreparationComplete(false, "Preparation for PrepareForMaintenance is completed"), + UnableToMigrate(false, "Migration for at least one VM didn't work"), + Ping(false, "Ping is received from the host"), + MaintenanceRequested(true, "PrepareForMaintenance requested by user"), + ManagementServerDown(false, "Management Server that the agent is connected is going down"), + WaitedTooLong(false, "Waited too long from the agent to reconnect on its own. Time to do HA"), + Remove(true, "Host is removed"), + Ready(false, "Host is ready for commands"); + + private final boolean isUserRequest; + private final String comment; + private Event(boolean isUserRequest, String comment) { + this.isUserRequest = isUserRequest; + this.comment = comment; + } + + public String getDescription() { + return comment; + } + + public boolean isUserRequest() { + return isUserRequest; + } + } + + public Status getNextStatus(Event e) { + return s_fsm.getNextState(this, e); + } + + public Status[] getFromStates(Event e) { + List from = s_fsm.getFromStates(this, e); + return from.toArray(new Status[from.size()]); + } + + public Set getPossibleEvents() { + return s_fsm.getPossibleEvents(this); + } + + public static String[] toStrings(Status... states) { + String[] strs = new String[states.length]; + for (int i = 0; i < states.length; i++) { + strs[i] = states[i].toString(); + } + return strs; + } + + protected static final StateMachine s_fsm = new StateMachine(); + static { + s_fsm.addTransition(null, Event.AgentConnected, Status.Connecting); + s_fsm.addTransition(Status.Connecting, Event.AgentConnected, Status.Connecting); + s_fsm.addTransition(Status.Connecting, Event.Ready, Status.Up); + s_fsm.addTransition(Status.Connecting, Event.PingTimeout, Status.Alert); + s_fsm.addTransition(Status.Connecting, Event.UpdateNeeded, Status.Updating); + s_fsm.addTransition(Status.Connecting, Event.MaintenanceRequested, Status.PrepareForMaintenance); + s_fsm.addTransition(Status.Connecting, Event.ShutdownRequested, Status.Disconnected); + s_fsm.addTransition(Status.Connecting, Event.HostDown, Status.Alert); + s_fsm.addTransition(Status.Connecting, Event.Ping, Status.Connecting); + s_fsm.addTransition(Status.Connecting, Event.ManagementServerDown, Status.Disconnected); + s_fsm.addTransition(Status.Connecting, Event.AgentDisconnected, Status.Alert); + s_fsm.addTransition(Status.Up, Event.PingTimeout, Status.Alert); + s_fsm.addTransition(Status.Up, Event.MaintenanceRequested, Status.PrepareForMaintenance); + s_fsm.addTransition(Status.Up, Event.AgentDisconnected, Status.Alert); + s_fsm.addTransition(Status.Up, Event.ShutdownRequested, Status.Disconnected); + s_fsm.addTransition(Status.Up, Event.HostDown, Status.Down); + s_fsm.addTransition(Status.Up, Event.Ping, Status.Up); + s_fsm.addTransition(Status.Up, Event.AgentConnected, Status.Connecting); + s_fsm.addTransition(Status.Up, Event.ManagementServerDown, Status.Disconnected); + s_fsm.addTransition(Status.Updating, Event.PingTimeout, Status.Alert); + s_fsm.addTransition(Status.Updating, Event.Ping, Status.Updating); + s_fsm.addTransition(Status.Updating, Event.AgentConnected, Status.Connecting); + s_fsm.addTransition(Status.Updating, Event.ManagementServerDown, Status.Disconnected); + s_fsm.addTransition(Status.Updating, Event.WaitedTooLong, Status.Alert); + s_fsm.addTransition(Status.PrepareForMaintenance, Event.ResetRequested, Status.Disconnected); + s_fsm.addTransition(Status.PrepareForMaintenance, Event.PreparationComplete, Status.Maintenance); + s_fsm.addTransition(Status.PrepareForMaintenance, Event.AgentDisconnected, Status.PrepareForMaintenance); + s_fsm.addTransition(Status.PrepareForMaintenance, Event.AgentConnected, Status.PrepareForMaintenance); + s_fsm.addTransition(Status.PrepareForMaintenance, Event.HostDown, Status.PrepareForMaintenance); + s_fsm.addTransition(Status.PrepareForMaintenance, Event.UnableToMigrate, Status.ErrorInMaintenance); + s_fsm.addTransition(Status.PrepareForMaintenance, Event.Remove, Status.Removed); + s_fsm.addTransition(Status.PrepareForMaintenance, Event.Ping, Status.PrepareForMaintenance); + s_fsm.addTransition(Status.PrepareForMaintenance, Event.ManagementServerDown, Status.PrepareForMaintenance); + s_fsm.addTransition(Status.ErrorInMaintenance, Event.MaintenanceRequested, Status.PrepareForMaintenance); + s_fsm.addTransition(Status.ErrorInMaintenance, Event.ResetRequested, Status.Disconnected); + s_fsm.addTransition(Status.ErrorInMaintenance, Event.HostDown, Status.ErrorInMaintenance); + s_fsm.addTransition(Status.ErrorInMaintenance, Event.AgentDisconnected, Status.ErrorInMaintenance); + s_fsm.addTransition(Status.ErrorInMaintenance, Event.AgentConnected, Status.ErrorInMaintenance); + s_fsm.addTransition(Status.ErrorInMaintenance, Event.Remove, Status.Removed); + s_fsm.addTransition(Status.ErrorInMaintenance, Event.UnableToMigrate, Status.ErrorInMaintenance); + s_fsm.addTransition(Status.ErrorInMaintenance, Event.PreparationComplete, Status.Maintenance); + s_fsm.addTransition(Status.ErrorInMaintenance, Event.Ping, Status.ErrorInMaintenance); + s_fsm.addTransition(Status.ErrorInMaintenance, Event.ManagementServerDown, Status.ErrorInMaintenance); + s_fsm.addTransition(Status.Maintenance, Event.ResetRequested, Status.Disconnected); + s_fsm.addTransition(Status.Maintenance, Event.AgentDisconnected, Status.Maintenance); + s_fsm.addTransition(Status.Maintenance, Event.HostDown, Status.Maintenance); + s_fsm.addTransition(Status.Maintenance, Event.Remove, Status.Removed); + s_fsm.addTransition(Status.Maintenance, Event.AgentConnected, Status.Maintenance); + s_fsm.addTransition(Status.Maintenance, Event.Ping, Status.Maintenance); + s_fsm.addTransition(Status.Maintenance, Event.ManagementServerDown, Status.Maintenance); + s_fsm.addTransition(Status.Disconnected, Event.PingTimeout, Status.Alert); + s_fsm.addTransition(Status.Disconnected, Event.AgentConnected, Status.Connecting); + s_fsm.addTransition(Status.Disconnected, Event.Ping, Status.Up); + s_fsm.addTransition(Status.Disconnected, Event.ManagementServerDown, Status.Disconnected); + s_fsm.addTransition(Status.Disconnected, Event.WaitedTooLong, Status.Alert); + s_fsm.addTransition(Status.Disconnected, Event.Remove, Status.Removed); + s_fsm.addTransition(Status.Down, Event.MaintenanceRequested, Status.PrepareForMaintenance); + s_fsm.addTransition(Status.Down, Event.AgentConnected, Status.Connecting); + s_fsm.addTransition(Status.Down, Event.Remove, Status.Removed); + s_fsm.addTransition(Status.Down, Event.ManagementServerDown, Status.Down); + s_fsm.addTransition(Status.Alert, Event.MaintenanceRequested, Status.PrepareForMaintenance); + s_fsm.addTransition(Status.Alert, Event.AgentConnected, Status.Connecting); + s_fsm.addTransition(Status.Alert, Event.Ping, Status.Up); + s_fsm.addTransition(Status.Alert, Event.Remove, Status.Removed); + s_fsm.addTransition(Status.Alert, Event.ManagementServerDown, Status.Alert); + } + + public static void main(String[] args) { + System.out.println("Finite State Machine for Host:"); + System.out.println(s_fsm.toString()); + } +} diff --git a/core/src/com/cloud/host/dao/DetailsDao.java b/core/src/com/cloud/host/dao/DetailsDao.java new file mode 100644 index 00000000000..26e17d50902 --- /dev/null +++ b/core/src/com/cloud/host/dao/DetailsDao.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host.dao; + +import java.util.Map; + +import com.cloud.host.DetailVO; +import com.cloud.utils.db.GenericDao; + +public interface DetailsDao extends GenericDao { + Map findDetails(long hostId); + + void persist(long hostId, Map details); + + DetailVO findDetail(long hostId, String name); +} diff --git a/core/src/com/cloud/host/dao/DetailsDaoImpl.java b/core/src/com/cloud/host/dao/DetailsDaoImpl.java new file mode 100644 index 00000000000..6bd554b683d --- /dev/null +++ b/core/src/com/cloud/host/dao/DetailsDaoImpl.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host.dao; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ejb.Local; + +import com.cloud.host.DetailVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value=DetailsDao.class) +public class DetailsDaoImpl extends GenericDaoBase implements DetailsDao { + protected final SearchBuilder HostSearch; + protected final SearchBuilder DetailSearch; + + protected DetailsDaoImpl() { + HostSearch = createSearchBuilder(); + HostSearch.and("hostId", HostSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostSearch.done(); + + DetailSearch = createSearchBuilder(); + DetailSearch.and("hostId", DetailSearch.entity().getHostId(), SearchCriteria.Op.EQ); + DetailSearch.and("name", DetailSearch.entity().getName(), SearchCriteria.Op.EQ); + DetailSearch.done(); + } + + @Override + public DetailVO findDetail(long hostId, String name) { + SearchCriteria sc = DetailSearch.create(); + sc.setParameters("hostId", hostId); + sc.setParameters("name", name); + + return findOneBy(sc); + } + + @Override + public Map findDetails(long hostId) { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("hostId", hostId); + + List results = search(sc, null); + Map details = new HashMap(results.size()); + for (DetailVO result : results) { + details.put(result.getName(), result.getValue()); + } + return details; + } + + @Override + public void persist(long hostId, Map details) { + Transaction txn = Transaction.currentTxn(); + txn.start(); + SearchCriteria sc = HostSearch.create(); + sc.setParameters("hostId", hostId); + delete(sc); + + for (Map.Entry detail : details.entrySet()) { + DetailVO vo = new DetailVO(hostId, detail.getKey(), detail.getValue()); + persist(vo); + } + txn.commit(); + } +} diff --git a/core/src/com/cloud/host/dao/HostDao.java b/core/src/com/cloud/host/dao/HostDao.java new file mode 100644 index 00000000000..6bccaa4a4ea --- /dev/null +++ b/core/src/com/cloud/host/dao/HostDao.java @@ -0,0 +1,141 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.Host.Type; +import com.cloud.host.Status.Event; +import com.cloud.info.RunningHostCountInfo; +import com.cloud.utils.db.GenericDao; + +/** + * Data Access Object for server + * + */ +public interface HostDao extends GenericDao { + List listBy(Host.Type type, Long clusterId, Long podId, long dcId); + + long countBy(long podId, Status... statuses); + + List listByDataCenter(long dcId); + List listByHostPod(long podId); + List listByStatus(Status... status); + List listBy(Host.Type type, long dcId); + HostVO findSecondaryStorageHost(long dcId); + List listByCluster(long clusterId); + /** + * Lists all secondary storage hosts, across all zones + * @return list of Hosts + */ + List listSecondaryStorageHosts(); + + /** + * Mark all hosts in Up or Orphaned state as disconnected. This method + * is used at AgentManager startup to reset all of the connections. + * + * @param msId management server id. + * @param statuses states of the host. + */ + void markHostsAsDisconnected(long msId, Status... states); + + List findLostHosts(long timeout); + + List findHostsLike(String hostName); + + /** + * Find hosts that are directly connected. + */ + List findDirectlyConnectedHosts(); + + List findDirectAgentToLoad(long msid, long lastPingSecondsAfter, Long limit); + + + /** + * Mark the host as disconnected if it is in one of these states. + * The management server id is set to null. + * The lastPinged timestamp is set to current. + * The state is set to the state passed in. + * The disconnectedOn timestamp is set to current. + * + * @param host host to be marked + * @param state state to be set to. + * @param ifStates only if it is one of these states. + * @return true if it's updated; false if not. + */ + boolean disconnect(HostVO host, Event event, long msId); + + boolean connect(HostVO host, long msId); + + HostVO findByStorageIpAddressInDataCenter(long dcId, String privateIpAddress); + HostVO findByPrivateIpAddressInDataCenter(long dcId, String privateIpAddress); + + /** + * find a host by its mac address + * @param macAddress + * @return HostVO or null if not found. + */ + public HostVO findByGuid(String macAddress); + + + /** + * find all hosts of a certain type in a data center + * @param type + * @param routingCapable + * @param dcId + * @return + */ + List listByTypeDataCenter(Host.Type type, long dcId); + + /** + * find all hosts of a particular type + * @param type + * @return + */ + List listByType(Type type, boolean routingCapable); + + /** + * Find hosts that have not responded to a ping regardless of state + * @param timeout + * @param type + * @return + */ + List findLostHosts2(long timeout, Type type); + + /** + * update the host and changes the status depending on the Event and + * the current status. If the status changed between + * @param host host object to change + * @param event event that happened. + * @param management server who's making this update + * @return true if updated; false if not. + */ + boolean updateStatus(HostVO host, Event event, long msId); + + List getRunningHostCounts(Date cutTime); + + long getNextSequence(long hostId); + + void loadDetails(HostVO host); + + +} diff --git a/core/src/com/cloud/host/dao/HostDaoImpl.java b/core/src/com/cloud/host/dao/HostDaoImpl.java new file mode 100644 index 00000000000..a58178f9c36 --- /dev/null +++ b/core/src/com/cloud/host/dao/HostDaoImpl.java @@ -0,0 +1,594 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.host.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; +import javax.persistence.TableGenerator; + +import org.apache.log4j.Logger; + +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.Host.Type; +import com.cloud.host.Status.Event; +import com.cloud.info.RunningHostCountInfo; +import com.cloud.utils.DateUtil; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.UpdateBuilder; +import com.cloud.utils.db.SearchCriteria.Func; +import com.cloud.utils.exception.CloudRuntimeException; + +@Local(value = { HostDao.class }) @DB(txn=false) +@TableGenerator(name="host_req_sq", table="op_host", pkColumnName="id", valueColumnName="sequence", allocationSize=1) +public class HostDaoImpl extends GenericDaoBase implements HostDao { + private static final Logger s_logger = Logger.getLogger(HostDaoImpl.class); + + protected final SearchBuilder TypePodDcStatusSearch; + + protected final SearchBuilder IdStatusSearch; + protected final SearchBuilder TypeDcSearch; + protected final SearchBuilder TypeDcStatusSearch; + protected final SearchBuilder LastPingedSearch; + protected final SearchBuilder LastPingedSearch2; + protected final SearchBuilder MsStatusSearch; + protected final SearchBuilder DcPrivateIpAddressSearch; + protected final SearchBuilder DcStorageIpAddressSearch; + + protected final SearchBuilder GuidSearch; + protected final SearchBuilder DcSearch; + protected final SearchBuilder PodSearch; + protected final SearchBuilder TypeSearch; + protected final SearchBuilder StatusSearch; + protected final SearchBuilder NameLikeSearch; + protected final SearchBuilder SequenceSearch; + protected final SearchBuilder DirectlyConnectedSearch; + protected final SearchBuilder UnmanagedDirectConnectSearch; + protected final GenericSearchBuilder MaintenanceCountSearch; + protected final SearchBuilder ClusterSearch; + + protected final Attribute _statusAttr; + protected final Attribute _msIdAttr; + protected final Attribute _pingTimeAttr; + + protected final DetailsDaoImpl _detailsDao = ComponentLocator.inject(DetailsDaoImpl.class); + + public HostDaoImpl() { + + MaintenanceCountSearch = createSearchBuilder(Long.class); + MaintenanceCountSearch.and("pod", MaintenanceCountSearch.entity().getPodId(), SearchCriteria.Op.EQ); + MaintenanceCountSearch.select(null, Func.COUNT, null); + MaintenanceCountSearch.and("status", MaintenanceCountSearch.entity().getStatus(), SearchCriteria.Op.IN); + MaintenanceCountSearch.done(); + + TypePodDcStatusSearch = createSearchBuilder(); + HostVO entity = TypePodDcStatusSearch.entity(); + TypePodDcStatusSearch.and("type", entity.getType(), SearchCriteria.Op.EQ); + TypePodDcStatusSearch.and("pod", entity.getPodId(), SearchCriteria.Op.EQ); + TypePodDcStatusSearch.and("dc", entity.getDataCenterId(), SearchCriteria.Op.EQ); + TypePodDcStatusSearch.and("cluster", entity.getClusterId(), SearchCriteria.Op.EQ); + TypePodDcStatusSearch.and("status", entity.getStatus(), SearchCriteria.Op.EQ); + TypePodDcStatusSearch.done(); + + LastPingedSearch = createSearchBuilder(); + LastPingedSearch.and("ping", LastPingedSearch.entity().getLastPinged(), SearchCriteria.Op.LT); + LastPingedSearch.and("state", LastPingedSearch.entity().getStatus(), SearchCriteria.Op.IN); + LastPingedSearch.done(); + + LastPingedSearch2 = createSearchBuilder(); + LastPingedSearch2.and("ping", LastPingedSearch2.entity().getLastPinged(), SearchCriteria.Op.LT); + LastPingedSearch2.and("type", LastPingedSearch2.entity().getType(), SearchCriteria.Op.EQ); + LastPingedSearch2.done(); + + MsStatusSearch = createSearchBuilder(); + MsStatusSearch.and("ms", MsStatusSearch.entity().getManagementServerId(), SearchCriteria.Op.EQ); + MsStatusSearch.and("statuses", MsStatusSearch.entity().getStatus(), SearchCriteria.Op.IN); + MsStatusSearch.done(); + + TypeDcSearch = createSearchBuilder(); + TypeDcSearch.and("type", TypeDcSearch.entity().getType(), SearchCriteria.Op.EQ); + TypeDcSearch.and("dc", TypeDcSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + TypeDcSearch.done(); + + TypeDcStatusSearch = createSearchBuilder(); + TypeDcStatusSearch.and("type", TypeDcStatusSearch.entity().getType(), SearchCriteria.Op.EQ); + TypeDcStatusSearch.and("dc", TypeDcStatusSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + TypeDcStatusSearch.and("status", TypeDcStatusSearch.entity().getStatus(), SearchCriteria.Op.EQ); + TypeDcStatusSearch.done(); + + IdStatusSearch = createSearchBuilder(); + IdStatusSearch.and("id", IdStatusSearch.entity().getId(), SearchCriteria.Op.EQ); + IdStatusSearch.and("states", IdStatusSearch.entity().getStatus(), SearchCriteria.Op.IN); + IdStatusSearch.done(); + + DcPrivateIpAddressSearch = createSearchBuilder(); + DcPrivateIpAddressSearch.and("privateIpAddress", DcPrivateIpAddressSearch.entity().getPrivateIpAddress(), SearchCriteria.Op.EQ); + DcPrivateIpAddressSearch.and("dc", DcPrivateIpAddressSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DcPrivateIpAddressSearch.done(); + + DcStorageIpAddressSearch = createSearchBuilder(); + DcStorageIpAddressSearch.and("storageIpAddress", DcStorageIpAddressSearch.entity().getStorageIpAddress(), SearchCriteria.Op.EQ); + DcStorageIpAddressSearch.and("dc", DcStorageIpAddressSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DcStorageIpAddressSearch.done(); + + GuidSearch = createSearchBuilder(); + GuidSearch.and("guid", GuidSearch.entity().getGuid(), SearchCriteria.Op.EQ); + GuidSearch.done(); + + DcSearch = createSearchBuilder(); + DcSearch.and("dc", DcSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DcSearch.done(); + + ClusterSearch = createSearchBuilder(); + ClusterSearch.and("cluster", ClusterSearch.entity().getClusterId(), SearchCriteria.Op.EQ); + ClusterSearch.done(); + + PodSearch = createSearchBuilder(); + PodSearch.and("pod", PodSearch.entity().getPodId(), SearchCriteria.Op.EQ); + PodSearch.done(); + + TypeSearch = createSearchBuilder(); + TypeSearch.and("type", TypeSearch.entity().getType(), SearchCriteria.Op.EQ); + TypeSearch.done(); + + StatusSearch =createSearchBuilder(); + StatusSearch.and("status", StatusSearch.entity().getStatus(), SearchCriteria.Op.IN); + StatusSearch.done(); + + NameLikeSearch = createSearchBuilder(); + NameLikeSearch.and("name", NameLikeSearch.entity().getName(), SearchCriteria.Op.LIKE); + NameLikeSearch.done(); + + SequenceSearch = createSearchBuilder(); + SequenceSearch.and("id", SequenceSearch.entity().getId(), SearchCriteria.Op.EQ); +// SequenceSearch.addRetrieve("sequence", SequenceSearch.entity().getSequence()); + SequenceSearch.done(); + + DirectlyConnectedSearch = createSearchBuilder(); + DirectlyConnectedSearch.and("resource", DirectlyConnectedSearch.entity().getResource(), SearchCriteria.Op.NNULL); + DirectlyConnectedSearch.done(); + + UnmanagedDirectConnectSearch = createSearchBuilder(); + UnmanagedDirectConnectSearch.and("resource", UnmanagedDirectConnectSearch.entity().getResource(), SearchCriteria.Op.NNULL); + UnmanagedDirectConnectSearch.and("server", UnmanagedDirectConnectSearch.entity().getManagementServerId(), SearchCriteria.Op.NULL); + /* + UnmanagedDirectConnectSearch.op(SearchCriteria.Op.OR, "managementServerId", UnmanagedDirectConnectSearch.entity().getManagementServerId(), SearchCriteria.Op.EQ); + UnmanagedDirectConnectSearch.and("lastPinged", UnmanagedDirectConnectSearch.entity().getLastPinged(), SearchCriteria.Op.LTEQ); + UnmanagedDirectConnectSearch.cp(); + UnmanagedDirectConnectSearch.cp(); + */ + UnmanagedDirectConnectSearch.done(); + + _statusAttr = _allAttributes.get("status"); + _msIdAttr = _allAttributes.get("managementServerId"); + _pingTimeAttr = _allAttributes.get("lastPinged"); + + assert (_statusAttr != null && _msIdAttr != null && _pingTimeAttr != null) : "Couldn't find one of these attributes"; + } + + @Override + public long countBy(long podId, Status... statuses) { + SearchCriteria sc = MaintenanceCountSearch.create(); + + sc.setParameters("status", (Object[])statuses); + sc.setParameters("pod", podId); + + List rs = searchAll(sc, null); + if (rs.size() == 0) { + return 0; + } + + return rs.get(0); + } + + @Override + public HostVO findSecondaryStorageHost(long dcId) { + SearchCriteria sc = TypeDcSearch.create(); + sc.setParameters("type", Host.Type.SecondaryStorage); + sc.setParameters("dc", dcId); + List storageHosts = listActiveBy(sc); + + if (storageHosts == null || storageHosts.size() != 1) { + return null; + } else { + return storageHosts.get(0); + } + } + + @Override + public List listSecondaryStorageHosts() { + SearchCriteria sc = TypeSearch.create(); + sc.setParameters("type", Host.Type.SecondaryStorage); + List secondaryStorageHosts = listBy(sc); + + return secondaryStorageHosts; + } + + @Override + public List findDirectlyConnectedHosts() { + SearchCriteria sc = DirectlyConnectedSearch.create(); + return search(sc, null); + } + + @Override + public List findDirectAgentToLoad(long msid, long lastPingSecondsAfter, Long limit) { + SearchCriteria sc = UnmanagedDirectConnectSearch.create(); + // sc.setParameters("lastPinged", lastPingSecondsAfter); + //sc.setParameters("managementServerId", msid); + + return search(sc, new Filter(HostVO.class, "id", true, 0L, limit)); + } + + @Override + public void markHostsAsDisconnected(long msId, Status... states) { + SearchCriteria sc = MsStatusSearch.create(); + sc.setParameters("ms", msId); + sc.setParameters("statuses", (Object[])states); + + HostVO host = createForUpdate(); + host.setManagementServerId(null); + host.setDisconnectedOn(new Date()); + + UpdateBuilder ub = getUpdateBuilder(host); + ub.set(host, "status", Status.Disconnected); + + update(ub, sc, null); + } + + @Override + public List listBy(Host.Type type, Long clusterId, Long podId, long dcId) { + SearchCriteria sc = TypePodDcStatusSearch.create(); + sc.setParameters("type", type.toString()); + if (podId != null) { + sc.setParameters("pod", podId); + } + if (clusterId != null) { + sc.setParameters("cluster", clusterId); + } + sc.setParameters("dc", dcId); + sc.setParameters("status", Status.Up.toString()); + + return listActiveBy(sc); + } + + @Override + public List listByCluster(long clusterId) { + SearchCriteria sc = ClusterSearch.create(); + + sc.setParameters("cluster", clusterId); + + return listActiveBy(sc); + } + + @Override + public List listBy(Host.Type type, long dcId) { + SearchCriteria sc = TypeDcStatusSearch.create(); + sc.setParameters("type", type.toString()); + sc.setParameters("dc", dcId); + sc.setParameters("status", Status.Up.toString()); + + return listActiveBy(sc); + } + + @Override + public HostVO findByPrivateIpAddressInDataCenter(long dcId, String privateIpAddress) { + SearchCriteria sc = DcPrivateIpAddressSearch.create(); + sc.setParameters("dc", dcId); + sc.setParameters("privateIpAddress", privateIpAddress); + + return findOneActiveBy(sc); + } + + @Override + public HostVO findByStorageIpAddressInDataCenter(long dcId, String privateIpAddress) { + SearchCriteria sc = DcStorageIpAddressSearch.create(); + sc.setParameters("dc", dcId); + sc.setParameters("storageIpAddress", privateIpAddress); + + return findOneActiveBy(sc); + } + + @Override + public void loadDetails(HostVO host) { + Map details =_detailsDao.findDetails(host.getId()); + host.setDetails(details); + } + + @Override + public boolean updateStatus(HostVO host, Event event, long msId) { + Status oldStatus = host.getStatus(); + long oldPingTime = host.getLastPinged(); + Status newStatus = oldStatus.getNextStatus(event); + if ( host == null ) { + return false; + } + + if (newStatus == null) { + return false; + } + + SearchBuilder sb = createSearchBuilder(); + sb.and("status", sb.entity().getStatus(), SearchCriteria.Op.EQ); + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + if (newStatus.checkManagementServer()) { + sb.and("ping", sb.entity().getLastPinged(), SearchCriteria.Op.EQ); + sb.and().op("nullmsid", sb.entity().getManagementServerId(), SearchCriteria.Op.NULL); + sb.or("msid", sb.entity().getManagementServerId(), SearchCriteria.Op.EQ); + sb.closeParen(); + } + sb.done(); + + SearchCriteria sc = sb.create(); + + sc.setParameters("status", oldStatus); + sc.setParameters("id", host.getId()); + if (newStatus.checkManagementServer()) { + sc.setParameters("ping", oldPingTime); + sc.setParameters("msid", msId); + } + + UpdateBuilder ub = getUpdateBuilder(host); + ub.set(host, _statusAttr, newStatus); + if (newStatus.updateManagementServer()) { + if (newStatus.lostConnection()) { + ub.set(host, _msIdAttr, null); + } else { + ub.set(host, _msIdAttr, msId); + } + if( event.equals(Event.Ping) || event.equals(Event.AgentConnected)) { + ub.set(host, _pingTimeAttr, System.currentTimeMillis() >> 10); + } + } + + int result = update(ub, sc, null); + assert result <= 1 : "How can this update " + result + " rows? "; + + if (s_logger.isDebugEnabled() && result == 0) { + HostVO vo = findById(host.getId()); + assert vo != null : "How how how? : " + host.getId(); + + StringBuilder str = new StringBuilder("Unable to update host for event:").append(event.toString()); + str.append(". New=[status=").append(newStatus.toString()).append(":msid=").append(newStatus.lostConnection() ? "null" : msId).append(":lastpinged=").append(host.getLastPinged()).append("]"); + str.append("; Old=[status=").append(oldStatus.toString()).append(":msid=").append(msId).append(":lastpinged=").append(oldPingTime).append("]"); + str.append("; DB=[status=").append(vo.getStatus().toString()).append(":msid=").append(vo.getManagementServerId()).append(":lastpinged=").append(vo.getLastPinged()).append("]"); + s_logger.debug(str.toString()); + } + return result > 0; + } + + @Override + public boolean disconnect(HostVO host, Event event, long msId) { + host.setDisconnectedOn(new Date()); + if(event!=null && event.equals(Event.Remove)) { + host.setGuid(null); + host.setClusterId(null); + } + return updateStatus(host, event, msId); + } + + @Override @DB + public boolean connect(HostVO host, long msId) { + Transaction txn = Transaction.currentTxn(); + long id = host.getId(); + txn.start(); + + if (!updateStatus(host, Event.AgentConnected, msId)) { + return false; + } + + txn.commit(); + return true; + } + + @Override + public HostVO findByGuid(String guid) { + SearchCriteria sc = GuidSearch.create("guid", guid); + return findOneActiveBy(sc); + } + + @Override + public List findLostHosts(long timeout) { + SearchCriteria sc = LastPingedSearch.create(); + sc.setParameters("ping", timeout); + sc.setParameters("state", Status.Up.toString(), Status.Updating.toString(), + Status.Disconnected.toString(), Status.Down.toString()); + return listActiveBy(sc); + } + + public List findHostsLike(String hostName) { + SearchCriteria sc = NameLikeSearch.create(); + sc.setParameters("name", "%" + hostName + "%"); + return listActiveBy(sc); + } + + @Override + public List findLostHosts2(long timeout, Type type) { + SearchCriteria sc = LastPingedSearch2.create(); + sc.setParameters("ping", timeout); + sc.setParameters("type", type.toString()); + return listActiveBy(sc); + } + + @Override + public List listByDataCenter(long dcId) { + SearchCriteria sc = DcSearch.create("dc", dcId); + return listActiveBy(sc); + } + + public List listByHostPod(long podId) { + SearchCriteria sc = PodSearch.create("pod", podId); + return listActiveBy(sc); + } + + @Override + public List listByStatus(Status... status) { + SearchCriteria sc = StatusSearch.create(); + sc.setParameters("status", (Object[])status); + return listActiveBy(sc); + } + + @Override + public List listByTypeDataCenter(Type type, long dcId) { + SearchCriteria sc = TypeDcSearch.create(); + sc.setParameters("type", type.toString()); + sc.setParameters("dc", dcId); + + return listActiveBy(sc); + } + + @Override + public List listByType(Type type, boolean routingCapable) { + SearchCriteria sc = TypeSearch.create(); + sc.setParameters("type", type.toString()); + + if (routingCapable) { + sc.addAnd("routing_capbable", SearchCriteria.Op.EQ, Integer.valueOf(1)); + } + + return listBy(sc); + } + + protected void saveDetails(HostVO host) { + Map details = host.getDetails(); + if (details == null) { + return; + } + _detailsDao.persist(host.getId(), details); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + if (!super.configure(name, params)) { + return false; + } + + return true; + } + + @Override @DB + public HostVO persist(HostVO host) { + final String InsertSequenceSql = "INSERT INTO op_host(id) VALUES(?)"; + + Transaction txn = Transaction.currentTxn(); + txn.start(); + + HostVO dbHost = super.persist(host); + + try { + PreparedStatement pstmt = txn.prepareAutoCloseStatement(InsertSequenceSql); + pstmt.setLong(1, dbHost.getId()); + pstmt.executeUpdate(); + } catch (SQLException e) { + throw new CloudRuntimeException("Unable to persist the sequence number for this host"); + } + + saveDetails(host); + loadDetails(dbHost); + + txn.commit(); + + return dbHost; + } + + @Override @DB + public boolean update(Long hostId, HostVO host) { + Transaction txn = Transaction.currentTxn(); + txn.start(); + + boolean persisted = super.update(hostId, host); + if (!persisted) { + return persisted; + } + + saveDetails(host); + + txn.commit(); + + return persisted; + } + + @Override @DB + public List getRunningHostCounts(Date cutTime) { + String sql = "select * from (" + + "select h.data_center_id, h.type, count(*) as count from host as h INNER JOIN mshost as m ON h.mgmt_server_id=m.msid " + + "where h.status='Up' and h.type='SecondaryStorage' and m.last_update > ? " + + "group by h.data_center_id, h.type " + + "UNION ALL " + + "select h.data_center_id, h.type, count(*) as count from host as h INNER JOIN mshost as m ON h.mgmt_server_id=m.msid " + + "where h.status='Up' and h.type='Routing' and m.last_update > ? " + + "group by h.data_center_id, h.type) as t " + + "ORDER by t.data_center_id, t.type"; + + ArrayList l = new ArrayList(); + + Transaction txn = Transaction.currentTxn();; + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(sql); + String gmtCutTime = DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), cutTime); + pstmt.setString(1, gmtCutTime); + pstmt.setString(2, gmtCutTime); + + ResultSet rs = pstmt.executeQuery(); + while(rs.next()) { + RunningHostCountInfo info = new RunningHostCountInfo(); + info.setDcId(rs.getLong(1)); + info.setHostType(rs.getString(2)); + info.setCount(rs.getInt(3)); + + l.add(info); + } + } catch (SQLException e) { + } catch (Throwable e) { + } + return l; + } + + @Override + public long getNextSequence(long hostId) { + if (s_logger.isTraceEnabled()) { + s_logger.trace("getNextSequence(), hostId: " + hostId); + } + + TableGenerator tg = _tgs.get("host_req_sq"); + assert tg != null : "how can this be wrong!"; + + return s_seqFetcher.getNextSequence(Long.class, tg, hostId); + } +} + + + diff --git a/core/src/com/cloud/hypervisor/vmware/resource/VmwareResource.java b/core/src/com/cloud/hypervisor/vmware/resource/VmwareResource.java new file mode 100644 index 00000000000..0df9c7df36c --- /dev/null +++ b/core/src/com/cloud/hypervisor/vmware/resource/VmwareResource.java @@ -0,0 +1,122 @@ +package com.cloud.hypervisor.vmware.resource; + +import java.util.Map; + +import javax.naming.ConfigurationException; + +import com.cloud.agent.IAgentControl; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.agent.api.storage.CopyVolumeAnswer; +import com.cloud.agent.api.storage.CopyVolumeCommand; +import com.cloud.agent.api.storage.CreateAnswer; +import com.cloud.agent.api.storage.CreateCommand; +import com.cloud.agent.api.storage.DestroyCommand; +import com.cloud.agent.api.storage.DownloadAnswer; +import com.cloud.agent.api.storage.PrimaryStorageDownloadCommand; +import com.cloud.agent.api.storage.ShareAnswer; +import com.cloud.agent.api.storage.ShareCommand; +import com.cloud.host.Host.Type; +import com.cloud.resource.ServerResource; +import com.cloud.storage.resource.StoragePoolResource; + +public class VmwareResource implements StoragePoolResource, ServerResource { + + @Override + public DownloadAnswer execute(PrimaryStorageDownloadCommand cmd) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Answer execute(DestroyCommand cmd) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ShareAnswer execute(ShareCommand cmd) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CopyVolumeAnswer execute(CopyVolumeCommand cmd) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CreateAnswer execute(CreateCommand cmd) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void disconnected() { + // TODO Auto-generated method stub + + } + + @Override + public Answer executeRequest(Command cmd) { + // TODO Auto-generated method stub + return null; + } + + @Override + public IAgentControl getAgentControl() { + // TODO Auto-generated method stub + return null; + } + + @Override + public PingCommand getCurrentStatus(long id) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Type getType() { + // TODO Auto-generated method stub + return null; + } + + @Override + public StartupCommand[] initialize() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setAgentControl(IAgentControl agentControl) { + // TODO Auto-generated method stub + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + + // TODO Auto-generated method stub + return true; + } + + @Override + public String getName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean start() { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean stop() { + // TODO Auto-generated method stub + return false; + } +} diff --git a/core/src/com/cloud/hypervisor/xen/resource/CitrixResourceBase.java b/core/src/com/cloud/hypervisor/xen/resource/CitrixResourceBase.java new file mode 100644 index 00000000000..1f5ca0f6827 --- /dev/null +++ b/core/src/com/cloud/hypervisor/xen/resource/CitrixResourceBase.java @@ -0,0 +1,6164 @@ +/** +: * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.hypervisor.xen.resource; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.log4j.Logger; +import org.apache.xmlrpc.XmlRpcException; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import com.cloud.agent.IAgentControl; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.AttachIsoCommand; +import com.cloud.agent.api.AttachVolumeAnswer; +import com.cloud.agent.api.AttachVolumeCommand; +import com.cloud.agent.api.BackupSnapshotAnswer; +import com.cloud.agent.api.BackupSnapshotCommand; +import com.cloud.agent.api.CheckHealthAnswer; +import com.cloud.agent.api.CheckHealthCommand; +import com.cloud.agent.api.CheckOnHostAnswer; +import com.cloud.agent.api.CheckOnHostCommand; +import com.cloud.agent.api.CheckVirtualMachineAnswer; +import com.cloud.agent.api.CheckVirtualMachineCommand; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.CreatePrivateTemplateFromSnapshotCommand; +import com.cloud.agent.api.CreateVolumeFromSnapshotAnswer; +import com.cloud.agent.api.CreateVolumeFromSnapshotCommand; +import com.cloud.agent.api.DeleteSnapshotBackupAnswer; +import com.cloud.agent.api.DeleteSnapshotBackupCommand; +import com.cloud.agent.api.DeleteSnapshotsDirCommand; +import com.cloud.agent.api.DeleteStoragePoolCommand; +import com.cloud.agent.api.GetHostStatsAnswer; +import com.cloud.agent.api.GetHostStatsCommand; +import com.cloud.agent.api.GetStorageStatsAnswer; +import com.cloud.agent.api.GetStorageStatsCommand; +import com.cloud.agent.api.GetVmStatsAnswer; +import com.cloud.agent.api.GetVmStatsCommand; +import com.cloud.agent.api.GetVncPortAnswer; +import com.cloud.agent.api.GetVncPortCommand; +import com.cloud.agent.api.HostStatsEntry; +import com.cloud.agent.api.MaintainAnswer; +import com.cloud.agent.api.MaintainCommand; +import com.cloud.agent.api.ManageSnapshotAnswer; +import com.cloud.agent.api.ManageSnapshotCommand; +import com.cloud.agent.api.MigrateAnswer; +import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.ModifySshKeysCommand; +import com.cloud.agent.api.ModifyStoragePoolAnswer; +import com.cloud.agent.api.ModifyStoragePoolCommand; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.PingRoutingCommand; +import com.cloud.agent.api.PingRoutingWithNwGroupsCommand; +import com.cloud.agent.api.PingTestCommand; +import com.cloud.agent.api.PoolEjectCommand; +import com.cloud.agent.api.PrepareForMigrationAnswer; +import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.ReadyAnswer; +import com.cloud.agent.api.ReadyCommand; +import com.cloud.agent.api.RebootAnswer; +import com.cloud.agent.api.RebootCommand; +import com.cloud.agent.api.RebootRouterCommand; +import com.cloud.agent.api.SetupAnswer; +import com.cloud.agent.api.SetupCommand; +import com.cloud.agent.api.StartAnswer; +import com.cloud.agent.api.StartCommand; +import com.cloud.agent.api.StartConsoleProxyAnswer; +import com.cloud.agent.api.StartConsoleProxyCommand; +import com.cloud.agent.api.StartRouterAnswer; +import com.cloud.agent.api.StartRouterCommand; +import com.cloud.agent.api.StartSecStorageVmAnswer; +import com.cloud.agent.api.StartSecStorageVmCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.agent.api.StartupRoutingCommand; +import com.cloud.agent.api.StartupStorageCommand; +import com.cloud.agent.api.StopAnswer; +import com.cloud.agent.api.StopCommand; +import com.cloud.agent.api.StoragePoolInfo; +import com.cloud.agent.api.ValidateSnapshotAnswer; +import com.cloud.agent.api.ValidateSnapshotCommand; +import com.cloud.agent.api.VmStatsEntry; +import com.cloud.agent.api.proxy.CheckConsoleProxyLoadCommand; +import com.cloud.agent.api.proxy.ConsoleProxyLoadAnswer; +import com.cloud.agent.api.proxy.WatchConsoleProxyLoadCommand; +import com.cloud.agent.api.routing.DhcpEntryCommand; +import com.cloud.agent.api.routing.IPAssocCommand; +import com.cloud.agent.api.routing.LoadBalancerCfgCommand; +import com.cloud.agent.api.routing.SavePasswordCommand; +import com.cloud.agent.api.routing.SetFirewallRuleCommand; +import com.cloud.agent.api.routing.VmDataCommand; +import com.cloud.agent.api.storage.CopyVolumeAnswer; +import com.cloud.agent.api.storage.CopyVolumeCommand; +import com.cloud.agent.api.storage.CreateAnswer; +import com.cloud.agent.api.storage.CreateCommand; +import com.cloud.agent.api.storage.CreatePrivateTemplateAnswer; +import com.cloud.agent.api.storage.CreatePrivateTemplateCommand; +import com.cloud.agent.api.storage.DestroyCommand; +import com.cloud.agent.api.storage.DownloadAnswer; +import com.cloud.agent.api.storage.PrimaryStorageDownloadCommand; +import com.cloud.agent.api.storage.ShareAnswer; +import com.cloud.agent.api.storage.ShareCommand; +import com.cloud.agent.api.to.DiskCharacteristicsTO; +import com.cloud.agent.api.to.StoragePoolTO; +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.exception.InternalErrorException; +import com.cloud.host.Host.Type; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.resource.ServerResource; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.StorageLayer; +import com.cloud.storage.StoragePoolVO; +import com.cloud.storage.Volume.StorageResourceType; +import com.cloud.storage.Volume.VolumeType; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.resource.StoragePoolResource; +import com.cloud.storage.template.TemplateInfo; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; +import com.cloud.utils.script.Script; +import com.cloud.vm.ConsoleProxyVO; +import com.cloud.vm.DomainRouter; +import com.cloud.vm.SecondaryStorageVmVO; +import com.cloud.vm.State; +import com.cloud.vm.VirtualMachineName; +import com.trilead.ssh2.SCPClient; +import com.xensource.xenapi.APIVersion; +import com.xensource.xenapi.Bond; +import com.xensource.xenapi.Connection; +import com.xensource.xenapi.Console; +import com.xensource.xenapi.Host; +import com.xensource.xenapi.HostCpu; +import com.xensource.xenapi.HostMetrics; +import com.xensource.xenapi.Network; +import com.xensource.xenapi.PBD; +import com.xensource.xenapi.PIF; +import com.xensource.xenapi.Pool; +import com.xensource.xenapi.SR; +import com.xensource.xenapi.Session; +import com.xensource.xenapi.Types; +import com.xensource.xenapi.Types.BadServerResponse; +import com.xensource.xenapi.Types.IpConfigurationMode; +import com.xensource.xenapi.Types.VmPowerState; +import com.xensource.xenapi.Types.XenAPIException; +import com.xensource.xenapi.VBD; +import com.xensource.xenapi.VDI; +import com.xensource.xenapi.VIF; +import com.xensource.xenapi.VLAN; +import com.xensource.xenapi.VM; +import com.xensource.xenapi.VMGuestMetrics; +import com.xensource.xenapi.XenAPIObject; + +/** + * Encapsulates the interface to the XenServer API. + * + */ +@Local(value = ServerResource.class) +public abstract class CitrixResourceBase implements StoragePoolResource, ServerResource { + private static final Logger s_logger = Logger.getLogger(CitrixResourceBase.class); + protected static final XenServerConnectionPool _connPool = XenServerConnectionPool.getInstance(); + protected static final int MB = 1024 * 1024; + protected String _name; + protected String _username; + protected String _password; + protected final int _retry = 24; + protected final int _sleep = 10000; + protected long _dcId; + protected String _pod; + protected String _cluster; + protected HashMap _vms = new HashMap(71); + protected String _patchPath; + protected String _privateNetworkName; + protected String _linkLocalPrivateNetworkName; + protected String _publicNetworkName; + protected String _storageNetworkName1; + protected String _storageNetworkName2; + protected String _guestNetworkName; + protected int _wait; + protected IAgentControl _agentControl; + + protected final XenServerHost _host = new XenServerHost(); + + // Guest and Host Performance Statistics + protected boolean _collectHostStats = false; + protected String _consolidationFunction = "AVERAGE"; + protected int _pollingIntervalInSeconds = 60; + + protected StorageLayer _storage; + protected boolean _canBridgeFirewall = false; + protected HashMap _pools = new HashMap(5); + + public enum SRType { + NFS, LVM, ISCSI, ISO, LVMOISCSI; + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + public boolean equals(String type) { + return super.toString().equalsIgnoreCase(type); + } + } + + protected static HashMap s_statesTable; + protected String _localGateway; + static { + s_statesTable = new HashMap(); + s_statesTable.put(Types.VmPowerState.HALTED, State.Stopped); + s_statesTable.put(Types.VmPowerState.PAUSED, State.Running); + s_statesTable.put(Types.VmPowerState.RUNNING, State.Running); + s_statesTable.put(Types.VmPowerState.SUSPENDED, State.Running); + s_statesTable.put(Types.VmPowerState.UNKNOWN, State.Unknown); + s_statesTable.put(Types.VmPowerState.UNRECOGNIZED, State.Unknown); + } + private static HashMap _guestOsType = new HashMap(50); + static { + _guestOsType.put("CentOS 4.5 (32-bit)", "CentOS 4.5"); + _guestOsType.put("CentOS 4.6 (32-bit)", "CentOS 4.6"); + _guestOsType.put("CentOS 4.7 (32-bit)", "CentOS 4.7"); + _guestOsType.put("CentOS 4.8 (32-bit)", "CentOS 4.8"); + _guestOsType.put("CentOS 5.0 (32-bit)", "CentOS 5.0"); + _guestOsType.put("CentOS 5.0 (64-bit)", "CentOS 5.0 x64"); + _guestOsType.put("CentOS 5.1 (32-bit)", "CentOS 5.1"); + _guestOsType.put("CentOS 5.1 (64-bit)", "CentOS 5.1 x64"); + _guestOsType.put("CentOS 5.2 (32-bit)", "CentOS 5.2"); + _guestOsType.put("CentOS 5.2 (64-bit)", "CentOS 5.2 x64"); + _guestOsType.put("CentOS 5.3 (32-bit)", "CentOS 5.3"); + _guestOsType.put("CentOS 5.3 (64-bit)", "CentOS 5.3 x64"); + _guestOsType.put("CentOS 5.4 (32-bit)", "CentOS 5.4"); + _guestOsType.put("CentOS 5.4 (64-bit)", "CentOS 5.4 x64"); + _guestOsType.put("Debian Lenny 5.0 (32-bit)", "Debian Lenny 5.0"); + _guestOsType.put("Oracle Enterprise Linux 5.0 (32-bit)", "Oracle Enterprise Linux 5.0"); + _guestOsType.put("Oracle Enterprise Linux 5.0 (64-bit)", "Oracle Enterprise Linux 5.0 x64"); + _guestOsType.put("Oracle Enterprise Linux 5.1 (32-bit)", "Oracle Enterprise Linux 5.1"); + _guestOsType.put("Oracle Enterprise Linux 5.1 (64-bit)", "Oracle Enterprise Linux 5.1 x64"); + _guestOsType.put("Oracle Enterprise Linux 5.2 (32-bit)", "Oracle Enterprise Linux 5.2"); + _guestOsType.put("Oracle Enterprise Linux 5.2 (64-bit)", "Oracle Enterprise Linux 5.2 x64"); + _guestOsType.put("Oracle Enterprise Linux 5.3 (32-bit)", "Oracle Enterprise Linux 5.3"); + _guestOsType.put("Oracle Enterprise Linux 5.3 (64-bit)", "Oracle Enterprise Linux 5.3 x64"); + _guestOsType.put("Oracle Enterprise Linux 5.4 (32-bit)", "Oracle Enterprise Linux 5.4"); + _guestOsType.put("Oracle Enterprise Linux 5.4 (64-bit)", "Oracle Enterprise Linux 5.4 x64"); + _guestOsType.put("Red Hat Enterprise Linux 4.5 (32-bit)", "Red Hat Enterprise Linux 4.5"); + _guestOsType.put("Red Hat Enterprise Linux 4.6 (32-bit)", "Red Hat Enterprise Linux 4.6"); + _guestOsType.put("Red Hat Enterprise Linux 4.7 (32-bit)", "Red Hat Enterprise Linux 4.7"); + _guestOsType.put("Red Hat Enterprise Linux 4.8 (32-bit)", "Red Hat Enterprise Linux 4.8"); + _guestOsType.put("Red Hat Enterprise Linux 5.0 (32-bit)", "Red Hat Enterprise Linux 5.0"); + _guestOsType.put("Red Hat Enterprise Linux 5.0 (64-bit)", "Red Hat Enterprise Linux 5.0 x64"); + _guestOsType.put("Red Hat Enterprise Linux 5.1 (32-bit)", "Red Hat Enterprise Linux 5.1"); + _guestOsType.put("Red Hat Enterprise Linux 5.1 (64-bit)", "Red Hat Enterprise Linux 5.1 x64"); + _guestOsType.put("Red Hat Enterprise Linux 5.2 (32-bit)", "Red Hat Enterprise Linux 5.2"); + _guestOsType.put("Red Hat Enterprise Linux 5.2 (64-bit)", "Red Hat Enterprise Linux 5.2 x64"); + _guestOsType.put("Red Hat Enterprise Linux 5.3 (32-bit)", "Red Hat Enterprise Linux 5.3"); + _guestOsType.put("Red Hat Enterprise Linux 5.3 (64-bit)", "Red Hat Enterprise Linux 5.3 x64"); + _guestOsType.put("Red Hat Enterprise Linux 5.4 (32-bit)", "Red Hat Enterprise Linux 5.4"); + _guestOsType.put("Red Hat Enterprise Linux 5.4 (64-bit)", "Red Hat Enterprise Linux 5.4 x64"); + _guestOsType.put("SUSE Linux Enterprise Server 9 SP4 (32-bit)", "SUSE Linux Enterprise Server 9 SP4"); + _guestOsType.put("SUSE Linux Enterprise Server 10 SP1 (32-bit)", "SUSE Linux Enterprise Server 10 SP1"); + _guestOsType.put("SUSE Linux Enterprise Server 10 SP1 (64-bit)", "SUSE Linux Enterprise Server 10 SP1 x64"); + _guestOsType.put("SUSE Linux Enterprise Server 10 SP2 (32-bit)", "SUSE Linux Enterprise Server 10 SP2"); + _guestOsType.put("SUSE Linux Enterprise Server 10 SP2 (64-bit)", "SUSE Linux Enterprise Server 10 SP2 x64"); + _guestOsType.put("SUSE Linux Enterprise Server 10 SP3 (64-bit)", "Other install media"); + _guestOsType.put("SUSE Linux Enterprise Server 11 (32-bit)", "SUSE Linux Enterprise Server 11"); + _guestOsType.put("SUSE Linux Enterprise Server 11 (64-bit)", "SUSE Linux Enterprise Server 11 x64"); + _guestOsType.put("Windows 7 (32-bit)", "Windows 7"); + _guestOsType.put("Windows 7 (64-bit)", "Windows 7 x64"); + _guestOsType.put("Windows Server 2003 (32-bit)", "Windows Server 2003"); + _guestOsType.put("Windows Server 2003 (64-bit)", "Windows Server 2003 x64"); + _guestOsType.put("Windows Server 2008 (32-bit)", "Windows Server 2008"); + _guestOsType.put("Windows Server 2008 (64-bit)", "Windows Server 2008 x64"); + _guestOsType.put("Windows Server 2008 R2 (64-bit)", "Windows Server 2008 R2 x64"); + _guestOsType.put("Windows 2000 SP4 (32-bit)", "Windows 2000 SP4"); + _guestOsType.put("Windows Vista (32-bit)", "Windows Vista"); + _guestOsType.put("Windows XP SP2 (32-bit)", "Windows XP SP2"); + _guestOsType.put("Windows XP SP3 (32-bit)", "Windows XP SP3"); + _guestOsType.put("Other install media", "Other install media"); + + } + + protected boolean isRefNull(XenAPIObject object) { + return (object == null || object.toWireString().equals("OpaqueRef:NULL")); + } + + @Override + public void disconnected() { + s_logger.debug("Logging out of " + _host.uuid); + if (_host.pool != null) { + _connPool.disconnect(_host.uuid, _host.pool); + _host.pool = null; + } + } + + protected void destroyStoppedVm() { + Map vmentries = null; + Connection conn = getConnection(); + for (int i = 0; i < 2; i++) { + try { + vmentries = VM.getAllRecords(conn); + break; + } catch (final Throwable e) { + s_logger.warn("Unable to get vms", e); + } + try { + Thread.sleep(1000); + } catch (final InterruptedException ex) { + + } + } + if (vmentries == null) { + return; + } + for (Map.Entry vmentry : vmentries.entrySet()) { + VM.Record record = vmentry.getValue(); + if (record.isControlDomain || record.isASnapshot || record.isATemplate) { + continue; // Skip DOM0 + } + if (record.powerState != Types.VmPowerState.HALTED) { + continue; + } + + try { + if (isRefNull(record.affinity) || !record.affinity.getUuid(conn).equals(_host.uuid)) { + continue; + } + vmentry.getKey().destroy(conn); + } catch (Exception e) { + String msg = "VM destroy failed for " + record.nameLabel + " due to " + e.getMessage(); + s_logger.warn(msg, e); + } + } + } + + protected void cleanupDiskMounts() { + Connection conn = getConnection(); + + Map srs; + try { + srs = SR.getAllRecords(conn); + } catch (XenAPIException e) { + s_logger.warn("Unable to get the SRs " + e.toString(), e); + throw new CloudRuntimeException("Unable to get SRs " + e.toString(), e); + } catch (XmlRpcException e) { + throw new CloudRuntimeException("Unable to get SRs " + e.getMessage()); + } + + for (Map.Entry sr : srs.entrySet()) { + SR.Record rec = sr.getValue(); + if (SRType.NFS.equals(rec.type) || (SRType.ISO.equals(rec.type) && rec.nameLabel.endsWith("iso"))) { + if (rec.PBDs == null || rec.PBDs.size() == 0) { + cleanSR(sr.getKey(), rec); + continue; + } + + for (PBD pbd : rec.PBDs) { + + if (isRefNull(pbd)) { + continue; + } + PBD.Record pbdr = null; + try { + pbdr = pbd.getRecord(conn); + } catch (XenAPIException e) { + s_logger.warn("Unable to get pbd record " + e.toString()); + } catch (XmlRpcException e) { + s_logger.warn("Unable to get pbd record " + e.getMessage()); + } + + if (pbdr == null) { + continue; + } + + try { + if (pbdr.host.getUuid(conn).equals(_host.uuid)) { + if (!currentlyAttached(sr.getKey(), rec, pbd, pbdr)) { + pbd.unplug(conn); + pbd.destroy(conn); + cleanSR(sr.getKey(), rec); + } else if (!pbdr.currentlyAttached) { + pbd.plug(conn); + } + } + + } catch (XenAPIException e) { + s_logger.warn("Catch XenAPIException due to" + e.toString(), e); + } catch (XmlRpcException e) { + s_logger.warn("Catch XmlRpcException due to" + e.getMessage(), e); + } + } + } + } + } + + protected Pair getVmByNameLabel(Connection conn, Host host, String nameLabel, boolean getRecord) throws XmlRpcException, XenAPIException { + Set vms = host.getResidentVMs(conn); + for (VM vm : vms) { + VM.Record rec = null; + String name = null; + if (getRecord) { + rec = vm.getRecord(conn); + name = rec.nameLabel; + } else { + name = vm.getNameLabel(conn); + } + if (name.equals(nameLabel)) { + return new Pair(vm, rec); + } + } + + return null; + } + + protected boolean currentlyAttached(SR sr, SR.Record rec, PBD pbd, PBD.Record pbdr) { + String status = null; + if (SRType.NFS.equals(rec.type)) { + status = callHostPlugin("vmops", "checkMount", "mount", rec.uuid); + } else if (SRType.LVMOISCSI.equals(rec.type) ) { + String scsiid = pbdr.deviceConfig.get("SCSIid"); + if (scsiid.isEmpty()) { + return false; + } + status = callHostPlugin("vmops", "checkIscsi", "scsiid", scsiid); + } else { + return true; + } + + if (status != null && status.equalsIgnoreCase("1")) { + s_logger.debug("currently attached " + pbdr.uuid); + return true; + } else { + s_logger.debug("currently not attached " + pbdr.uuid); + return false; + } + } + + protected boolean pingdomr(String host, String port) { + String status; + status = callHostPlugin("vmops", "pingdomr", "host", host, "port", port); + + if (status == null || status.isEmpty()) { + return false; + } + + return true; + + } + + protected boolean pingxenserver() { + String status; + status = callHostPlugin("vmops", "pingxenserver"); + + if (status == null || status.isEmpty()) { + return false; + } + + return true; + + } + + protected String logX(XenAPIObject obj, String msg) { + return new StringBuilder("Host ").append(_host.ip).append(" ").append(obj.toWireString()).append(": ").append(msg).toString(); + } + + protected void cleanSR(SR sr, SR.Record rec) { + Connection conn = getConnection(); + if (rec.VDIs != null) { + for (VDI vdi : rec.VDIs) { + + VDI.Record vdir; + try { + vdir = vdi.getRecord(conn); + } catch (XenAPIException e) { + s_logger.debug("Unable to get VDI: " + e.toString()); + continue; + } catch (XmlRpcException e) { + s_logger.debug("Unable to get VDI: " + e.getMessage()); + continue; + } + + if (vdir.VBDs == null) + continue; + + for (VBD vbd : vdir.VBDs) { + try { + VBD.Record vbdr = vbd.getRecord(conn); + VM.Record vmr = vbdr.VM.getRecord(conn); + if ((!isRefNull(vmr.residentOn) && vmr.residentOn.getUuid(conn).equals(_host.uuid)) + || (isRefNull(vmr.residentOn) && !isRefNull(vmr.affinity) && vmr.affinity.getUuid(conn).equals(_host.uuid))) { + if (vmr.powerState != VmPowerState.HALTED && vmr.powerState != VmPowerState.UNKNOWN && vmr.powerState != VmPowerState.UNRECOGNIZED) { + try { + vbdr.VM.hardShutdown(conn); + } catch (XenAPIException e) { + s_logger.debug("Shutdown hit error " + vmr.nameLabel + ": " + e.toString()); + } + } + try { + vbdr.VM.destroy(conn); + } catch (XenAPIException e) { + s_logger.debug("Destroy hit error " + vmr.nameLabel + ": " + e.toString()); + } catch (XmlRpcException e) { + s_logger.debug("Destroy hit error " + vmr.nameLabel + ": " + e.getMessage()); + } + vbd.destroy(conn); + break; + } + } catch (XenAPIException e) { + s_logger.debug("Unable to get VBD: " + e.toString()); + continue; + } catch (XmlRpcException e) { + s_logger.debug("Uanbel to get VBD: " + e.getMessage()); + continue; + } + } + } + } + + for (PBD pbd : rec.PBDs) { + PBD.Record pbdr = null; + try { + pbdr = pbd.getRecord(conn); + pbd.unplug(conn); + pbd.destroy(conn); + } catch (XenAPIException e) { + s_logger.warn("PBD " + ((pbdr != null) ? "(uuid:" + pbdr.uuid + ")" : "") + "destroy failed due to " + e.toString()); + } catch (XmlRpcException e) { + s_logger.warn("PBD " + ((pbdr != null) ? "(uuid:" + pbdr.uuid + ")" : "") + "destroy failed due to " + e.getMessage()); + } + } + + try { + rec = sr.getRecord(conn); + if (rec.PBDs == null || rec.PBDs.size() == 0) { + sr.forget(conn); + return; + } + } catch (XenAPIException e) { + s_logger.warn("Unable to retrieve sr again: " + e.toString(), e); + } catch (XmlRpcException e) { + s_logger.warn("Unable to retrieve sr again: " + e.getMessage(), e); + } + } + + @Override + public Answer executeRequest(Command cmd) { + if (cmd instanceof CreateCommand) { + return execute((CreateCommand) cmd); + } else if (cmd instanceof SetFirewallRuleCommand) { + return execute((SetFirewallRuleCommand) cmd); + } else if (cmd instanceof LoadBalancerCfgCommand) { + return execute((LoadBalancerCfgCommand) cmd); + } else if (cmd instanceof IPAssocCommand) { + return execute((IPAssocCommand) cmd); + } else if (cmd instanceof CheckConsoleProxyLoadCommand) { + return execute((CheckConsoleProxyLoadCommand) cmd); + } else if (cmd instanceof WatchConsoleProxyLoadCommand) { + return execute((WatchConsoleProxyLoadCommand) cmd); + } else if (cmd instanceof SavePasswordCommand) { + return execute((SavePasswordCommand) cmd); + } else if (cmd instanceof DhcpEntryCommand) { + return execute((DhcpEntryCommand) cmd); + } else if (cmd instanceof VmDataCommand) { + return execute((VmDataCommand) cmd); + } else if (cmd instanceof StartCommand) { + return execute((StartCommand) cmd); + } else if (cmd instanceof StartRouterCommand) { + return execute((StartRouterCommand) cmd); + } else if (cmd instanceof ReadyCommand) { + return execute((ReadyCommand) cmd); + } else if (cmd instanceof GetHostStatsCommand) { + return execute((GetHostStatsCommand) cmd); + } else if (cmd instanceof GetVmStatsCommand) { + return execute((GetVmStatsCommand) cmd); + } else if (cmd instanceof CheckHealthCommand) { + return execute((CheckHealthCommand) cmd); + } else if (cmd instanceof StopCommand) { + return execute((StopCommand) cmd); + } else if (cmd instanceof RebootRouterCommand) { + return execute((RebootRouterCommand) cmd); + } else if (cmd instanceof RebootCommand) { + return execute((RebootCommand) cmd); + } else if (cmd instanceof CheckVirtualMachineCommand) { + return execute((CheckVirtualMachineCommand) cmd); + } else if (cmd instanceof PrepareForMigrationCommand) { + return execute((PrepareForMigrationCommand) cmd); + } else if (cmd instanceof MigrateCommand) { + return execute((MigrateCommand) cmd); + } else if (cmd instanceof DestroyCommand) { + return execute((DestroyCommand) cmd); + } else if (cmd instanceof ShareCommand) { + return execute((ShareCommand) cmd); + } else if (cmd instanceof ModifyStoragePoolCommand) { + return execute((ModifyStoragePoolCommand) cmd); + } else if (cmd instanceof DeleteStoragePoolCommand) { + return execute((DeleteStoragePoolCommand) cmd); + } else if (cmd instanceof CopyVolumeCommand) { + return execute((CopyVolumeCommand) cmd); + } else if (cmd instanceof AttachVolumeCommand) { + return execute((AttachVolumeCommand) cmd); + } else if (cmd instanceof AttachIsoCommand) { + return execute((AttachIsoCommand) cmd); + } else if (cmd instanceof ValidateSnapshotCommand) { + return execute((ValidateSnapshotCommand) cmd); + } else if (cmd instanceof ManageSnapshotCommand) { + return execute((ManageSnapshotCommand) cmd); + } else if (cmd instanceof BackupSnapshotCommand) { + return execute((BackupSnapshotCommand) cmd); + } else if (cmd instanceof DeleteSnapshotBackupCommand) { + return execute((DeleteSnapshotBackupCommand) cmd); + } else if (cmd instanceof CreateVolumeFromSnapshotCommand) { + return execute((CreateVolumeFromSnapshotCommand) cmd); + } else if (cmd instanceof DeleteSnapshotsDirCommand) { + return execute((DeleteSnapshotsDirCommand) cmd); + } else if (cmd instanceof CreatePrivateTemplateCommand) { + return execute((CreatePrivateTemplateCommand) cmd); + } else if (cmd instanceof CreatePrivateTemplateFromSnapshotCommand) { + return execute((CreatePrivateTemplateFromSnapshotCommand) cmd); + } else if (cmd instanceof GetStorageStatsCommand) { + return execute((GetStorageStatsCommand) cmd); + } else if (cmd instanceof PrimaryStorageDownloadCommand) { + return execute((PrimaryStorageDownloadCommand) cmd); + } else if (cmd instanceof StartConsoleProxyCommand) { + return execute((StartConsoleProxyCommand) cmd); + } else if (cmd instanceof StartSecStorageVmCommand) { + return execute((StartSecStorageVmCommand) cmd); + } else if (cmd instanceof GetVncPortCommand) { + return execute((GetVncPortCommand) cmd); + } else if (cmd instanceof SetupCommand) { + return execute((SetupCommand) cmd); + } else if (cmd instanceof MaintainCommand) { + return execute((MaintainCommand) cmd); + } else if (cmd instanceof PingTestCommand) { + return execute((PingTestCommand) cmd); + } else if (cmd instanceof CheckOnHostCommand) { + return execute((CheckOnHostCommand) cmd); + } else if (cmd instanceof ModifySshKeysCommand) { + return execute((ModifySshKeysCommand) cmd); + } else if (cmd instanceof PoolEjectCommand) { + return execute((PoolEjectCommand) cmd); + } else { + return Answer.createUnsupportedCommandAnswer(cmd); + } + } + + protected Answer execute(ModifySshKeysCommand cmd) { + String publickey = cmd.getPubKey(); + String privatekey = cmd.getPrvKey(); + + com.trilead.ssh2.Connection sshConnection = new com.trilead.ssh2.Connection(_host.ip, 22); + try { + sshConnection.connect(null, 60000, 60000); + if (!sshConnection.authenticateWithPassword(_username, _password)) { + throw new Exception("Unable to authenticate"); + } + SCPClient scp = new SCPClient(sshConnection); + + scp.put(publickey.getBytes(), "id_rsa.pub", "/opt/xensource/bin", "0600"); + scp.put(privatekey.getBytes(), "id_rsa", "/opt/xensource/bin", "0600"); + scp.put(privatekey.getBytes(), "id_rsa.cloud", "/root/.ssh", "0600"); + return new Answer(cmd); + + } catch (Exception e) { + String msg = " scp ssh key failed due to " + e.toString() + " - " + e.getMessage(); + s_logger.warn(msg); + } finally { + sshConnection.close(); + } + return new Answer(cmd, false, "modifySshkeys failed"); + } + + private boolean doPingTest(final String computingHostIp) { + + String args = "-h " + computingHostIp; + String result = callHostPlugin("vmops", "pingtest", "args", args); + if (result == null || result.isEmpty()) + return false; + return true; + } + + protected CheckOnHostAnswer execute(CheckOnHostCommand cmd) { + return new CheckOnHostAnswer(cmd, null, "Not Implmeneted"); + } + + private boolean doPingTest(final String domRIp, final String vmIp) { + String args = "-i " + domRIp + " -p " + vmIp; + String result = callHostPlugin("vmops", "pingtest", "args", args); + if (result == null || result.isEmpty()) + return false; + return true; + } + + private Answer execute(PingTestCommand cmd) { + boolean result = false; + final String computingHostIp = cmd.getComputingHostIp(); // TODO, split the command into 2 types + + if (computingHostIp != null) { + result = doPingTest(computingHostIp); + } else { + result = doPingTest(cmd.getRouterIp(), cmd.getPrivateIp()); + } + + if (!result) { + return new Answer(cmd, false, "PingTestCommand failed"); + } + return new Answer(cmd); + } + + protected MaintainAnswer execute(MaintainCommand cmd) { + Connection conn = getConnection(); + try { + Pool pool = Pool.getByUuid(conn, _host.pool); + Pool.Record poolr = pool.getRecord(conn); + + Host.Record hostr = poolr.master.getRecord(conn); + if (!_host.uuid.equals(hostr.uuid)) { + s_logger.debug("Not the master node so just return ok: " + _host.ip); + return new MaintainAnswer(cmd); + } + Map hostMap = Host.getAllRecords(conn); + if (hostMap.size() == 1) { + s_logger.debug("There's no one to take over as master"); + return new MaintainAnswer(cmd, "Only master in the pool"); + } + Host newMaster = null; + Host.Record newMasterRecord = null; + for (Map.Entry entry : hostMap.entrySet()) { + if (!_host.uuid.equals(entry.getValue().uuid)) { + newMaster = entry.getKey(); + newMasterRecord = entry.getValue(); + s_logger.debug("New master for the XenPool is " + newMasterRecord.uuid + " : " + newMasterRecord.address); + try { + _connPool.switchMaster(_host.ip, _host.pool, conn, newMaster, _username, _password, _wait); + return new MaintainAnswer(cmd, "New Master is " + newMasterRecord.address); + } catch (XenAPIException e) { + s_logger.warn("Unable to switch the new master to " + newMasterRecord.uuid + ": " + newMasterRecord.address + " Trying again..."); + } catch (XmlRpcException e) { + s_logger.warn("Unable to switch the new master to " + newMasterRecord.uuid + ": " + newMasterRecord.address + " Trying again..."); + } + } + } + return new MaintainAnswer(cmd, false, "Unable to find an appropriate host to set as the new master"); + } catch (XenAPIException e) { + s_logger.warn("Unable to put server in maintainence mode", e); + return new MaintainAnswer(cmd, false, e.getMessage()); + } catch (XmlRpcException e) { + s_logger.warn("Unable to put server in maintainence mode", e); + return new MaintainAnswer(cmd, false, e.getMessage()); + } + } + + protected SetupAnswer execute(SetupCommand cmd) { + return new SetupAnswer(cmd); + } + + protected Answer execute(StartSecStorageVmCommand cmd) { + + final String vmName = cmd.getVmName(); + SecondaryStorageVmVO storage = cmd.getSecondaryStorageVmVO(); + try { + Connection conn = getConnection(); + + Network network = Network.getByUuid(conn, _host.privateNetwork); + + String bootArgs = cmd.getBootArgs(); + bootArgs += " zone=" + _dcId; + bootArgs += " pod=" + _pod; + bootArgs += " localgw=" + _localGateway; + String result = startSystemVM(vmName, storage.getVlanId(), network, cmd.getVolumes(), bootArgs, storage.getGuestMacAddress(), storage.getGuestIpAddress(), storage + .getPrivateMacAddress(), storage.getPublicMacAddress(), cmd.getProxyCmdPort(), storage.getRamSize()); + if (result == null) { + return new StartSecStorageVmAnswer(cmd); + } + return new StartSecStorageVmAnswer(cmd, result); + + } catch (Exception e) { + String msg = "Exception caught while starting router vm " + vmName + " due to " + e.getMessage(); + s_logger.warn(msg, e); + return new StartSecStorageVmAnswer(cmd, msg); + } + } + + protected Answer execute(final SetFirewallRuleCommand cmd) { + String args; + + if (cmd.isEnable()) { + args = "-A"; + } else { + args = "-D"; + } + + args += " -P " + cmd.getProtocol().toLowerCase(); + args += " -l " + cmd.getPublicIpAddress(); + args += " -p " + cmd.getPublicPort(); + args += " -n " + cmd.getRouterName(); + args += " -i " + cmd.getRouterIpAddress(); + args += " -r " + cmd.getPrivateIpAddress(); + args += " -d " + cmd.getPrivatePort(); + args += " -N " + cmd.getVlanNetmask(); + + String oldPrivateIP = cmd.getOldPrivateIP(); + String oldPrivatePort = cmd.getOldPrivatePort(); + + if (oldPrivateIP != null) { + args += " -w " + oldPrivateIP; + } + + if (oldPrivatePort != null) { + args += " -x " + oldPrivatePort; + } + + String result = callHostPlugin("vmops", "setFirewallRule", "args", args); + + if (result == null || result.isEmpty()) { + return new Answer(cmd, false, "SetFirewallRule failed"); + } + return new Answer(cmd); + } + + protected Answer execute(final LoadBalancerCfgCommand cmd) { + String routerIp = cmd.getRouterIp(); + + if (routerIp == null) { + return new Answer(cmd); + } + + String tmpCfgFilePath = "/tmp/" + cmd.getRouterIp().replace('.', '_') + ".cfg"; + String tmpCfgFileContents = ""; + for (int i = 0; i < cmd.getConfig().length; i++) { + tmpCfgFileContents += cmd.getConfig()[i]; + tmpCfgFileContents += "\n"; + } + + String result = callHostPlugin("vmops", "createFile", "filepath", tmpCfgFilePath, "filecontents", tmpCfgFileContents); + + if (result == null || result.isEmpty()) { + return new Answer(cmd, false, "LoadBalancerCfgCommand failed to create HA proxy cfg file."); + } + + String[] addRules = cmd.getAddFwRules(); + String[] removeRules = cmd.getRemoveFwRules(); + + String args = ""; + args += "-i " + routerIp; + args += " -f " + tmpCfgFilePath; + + StringBuilder sb = new StringBuilder(); + if (addRules.length > 0) { + for (int i = 0; i < addRules.length; i++) { + sb.append(addRules[i]).append(','); + } + + args += " -a " + sb.toString(); + } + + sb = new StringBuilder(); + if (removeRules.length > 0) { + for (int i = 0; i < removeRules.length; i++) { + sb.append(removeRules[i]).append(','); + } + + args += " -d " + sb.toString(); + } + + result = callHostPlugin("vmops", "setLoadBalancerRule", "args", args); + + if (result == null || result.isEmpty()) { + return new Answer(cmd, false, "LoadBalancerCfgCommand failed"); + } + + callHostPlugin("vmops", "deleteFile", "filepath", tmpCfgFilePath); + + return new Answer(cmd); + } + + protected synchronized Answer execute(final DhcpEntryCommand cmd) { + String args = "-r " + cmd.getRouterPrivateIpAddress(); + args += " -v " + cmd.getVmIpAddress(); + args += " -m " + cmd.getVmMac(); + args += " -n " + cmd.getVmName(); + String result = callHostPlugin("vmops", "saveDhcpEntry", "args", args); + if (result == null || result.isEmpty()) { + return new Answer(cmd, false, "DhcpEntry failed"); + } + return new Answer(cmd); + } + + protected Answer execute(final VmDataCommand cmd) { + String routerPrivateIpAddress = cmd.getRouterPrivateIpAddress(); + String vmIpAddress = cmd.getVmIpAddress(); + List vmData = cmd.getVmData(); + String[] vmDataArgs = new String[vmData.size() * 2 + 4]; + vmDataArgs[0] = "routerIP"; + vmDataArgs[1] = routerPrivateIpAddress; + vmDataArgs[2] = "vmIP"; + vmDataArgs[3] = vmIpAddress; + int i = 4; + for (String[] vmDataEntry : vmData) { + String folder = vmDataEntry[0]; + String file = vmDataEntry[1]; + String contents = (vmDataEntry[2] != null) ? vmDataEntry[2] : "none"; + + vmDataArgs[i] = folder + "," + file; + vmDataArgs[i + 1] = contents; + i += 2; + } + + String result = callHostPlugin("vmops", "vm_data", vmDataArgs); + + if (result == null || result.isEmpty()) { + return new Answer(cmd, false, "vm_data failed"); + } else { + return new Answer(cmd); + } + + } + + protected Answer execute(final SavePasswordCommand cmd) { + final String password = cmd.getPassword(); + final String routerPrivateIPAddress = cmd.getRouterPrivateIpAddress(); + final String vmName = cmd.getVmName(); + final String vmIpAddress = cmd.getVmIpAddress(); + final String local = vmName; + + // Run save_password_to_domr.sh + String args = "-r " + routerPrivateIPAddress; + args += " -v " + vmIpAddress; + args += " -p " + password; + args += " " + local; + String result = callHostPlugin("vmops", "savePassword", "args", args); + + if (result == null || result.isEmpty()) { + return new Answer(cmd, false, "savePassword failed"); + } + return new Answer(cmd); + } + + protected void assignPublicIpAddress(final String vmName, final String privateIpAddress, final String publicIpAddress, final boolean add, final boolean firstIP, + final boolean sourceNat, final String vlanId, final String vlanGateway, final String vlanNetmask, final String vifMacAddress) throws InternalErrorException { + + try { + Connection conn = getConnection(); + VM router = getVM(conn, vmName); + + // Determine the correct VIF on DomR to associate/disassociate the + // IP address with + VIF correctVif = getCorrectVif(router, vlanId); + + // If we are associating an IP address and DomR doesn't have a VIF + // for the specified vlan ID, we need to add a VIF + // If we are disassociating the last IP address in the VLAN, we need + // to remove a VIF + boolean addVif = false; + boolean removeVif = false; + if (add && correctVif == null) { + addVif = true; + } else if (!add && firstIP) { + removeVif = true; + } + + if (addVif) { + // Add a new VIF to DomR + String vifDeviceNum = getLowestAvailableVIFDeviceNum(router); + + if (vifDeviceNum == null) { + throw new InternalErrorException("There were no more available slots for a new VIF on router: " + router.getNameLabel(conn)); + } + + VIF.Record vifr = new VIF.Record(); + vifr.VM = router; + vifr.device = vifDeviceNum; + vifr.MAC = vifMacAddress; + + if ("untagged".equalsIgnoreCase(vlanId)) { + vifr.network = Network.getByUuid(conn, _host.publicNetwork); + } else { + Network vlanNetwork = enableVlanNetwork(Long.valueOf(vlanId), _host.publicNetwork, _host.publicPif); + + if (vlanNetwork == null) { + throw new InternalErrorException("Failed to enable VLAN network with tag: " + vlanId); + } + + vifr.network = vlanNetwork; + } + + correctVif = VIF.create(conn, vifr); + correctVif.plug(conn); + // Add iptables rule for network usage + networkUsage(privateIpAddress, "addVif", "eth" + correctVif.getDevice(conn)); + } + + if (correctVif == null) { + throw new InternalErrorException("Failed to find DomR VIF to associate/disassociate IP with."); + } + + String args; + if (add) { + args = "-A"; + } else { + args = "-D"; + } + if (sourceNat) { + args += " -f"; + } + args += " -i "; + args += privateIpAddress; + args += " -l "; + args += publicIpAddress; + args += " -c "; + args += "eth" + correctVif.getDevice(conn); + args += " -g "; + args += vlanGateway; + + String result = callHostPlugin("vmops", "ipassoc", "args", args); + if (result == null || result.isEmpty()) { + throw new InternalErrorException("Xen plugin \"ipassoc\" failed."); + } + + if (removeVif) { + Network network = correctVif.getNetwork(conn); + + // Mark this vif to be removed from network usage + networkUsage(privateIpAddress, "deleteVif", "eth" + correctVif.getDevice(conn)); + + // Remove the VIF from DomR + correctVif.unplug(conn); + correctVif.destroy(conn); + + // Disable the VLAN network if necessary + disableVlanNetwork(network); + } + + } catch (XenAPIException e) { + String msg = "Unable to assign public IP address due to " + e.toString(); + s_logger.warn(msg, e); + throw new InternalErrorException(msg); + } catch (final XmlRpcException e) { + String msg = "Unable to assign public IP address due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new InternalErrorException(msg); + } + + } + + protected String networkUsage(final String privateIpAddress, final String option, final String vif) { + + if (option.equals("get")) { + return "0:0"; + } + return null; + } + + protected Answer execute(final IPAssocCommand cmd) { + try { + assignPublicIpAddress(cmd.getRouterName(), cmd.getRouterIp(), cmd.getPublicIp(), cmd.isAdd(), cmd.isFirstIP(), cmd.isSourceNat(), cmd.getVlanId(), + cmd.getVlanGateway(), cmd.getVlanNetmask(), cmd.getVifMacAddress()); + } catch (InternalErrorException e) { + return new Answer(cmd, false, e.getMessage()); + } + + return new Answer(cmd); + } + + protected GetVncPortAnswer execute(GetVncPortCommand cmd) { + Connection conn = getConnection(); + + try { + Set vms = VM.getByNameLabel(conn, cmd.getName()); + return new GetVncPortAnswer(cmd, getVncPort(vms.iterator().next())); + } catch (XenAPIException e) { + s_logger.warn("Unable to get vnc port " + e.toString(), e); + return new GetVncPortAnswer(cmd, e.toString()); + } catch (Exception e) { + s_logger.warn("Unable to get vnc port ", e); + return new GetVncPortAnswer(cmd, e.getMessage()); + } + } + + protected StorageResourceType getStorageResourceType() { + return StorageResourceType.STORAGE_POOL; + } + + protected CheckHealthAnswer execute(CheckHealthCommand cmd) { + boolean result = pingxenserver(); + return new CheckHealthAnswer(cmd, result); + } + + + protected long[] getNetworkStats(String privateIP) { + String result = networkUsage(privateIP, "get", null); + long[] stats = new long[2]; + if (result != null) { + String[] splitResult = result.split(":"); + int i = 0; + while (i < splitResult.length - 1) { + stats[0] += (new Long(splitResult[i++])).longValue(); + stats[1] += (new Long(splitResult[i++])).longValue(); + } + } + return stats; + } + + /** + * This is the method called for getting the HOST stats + * + * @param cmd + * @return + */ + protected GetHostStatsAnswer execute(GetHostStatsCommand cmd) { + // Connection conn = getConnection(); + try { + + HostStatsEntry hostStats = getHostStats(cmd, cmd.getHostGuid(), cmd.getHostId()); + + return new GetHostStatsAnswer(cmd, hostStats); + } catch (Exception e) { + String msg = "Unable to get Host stats" + e.toString(); + s_logger.warn(msg, e); + return new GetHostStatsAnswer(cmd, null); + } + } + + protected HostStatsEntry getHostStats(GetHostStatsCommand cmd, String hostGuid, long hostId) { + + HostStatsEntry hostStats = new HostStatsEntry(hostId, 0, 0, 0, 0, "host", 0, 0, 0, 0); + Object[] rrdData = getRRDData(1); // call rrd method with 1 for host + + if (rrdData == null) { + return null; + } + + Integer numRows = (Integer) rrdData[0]; + Integer numColumns = (Integer) rrdData[1]; + Node legend = (Node) rrdData[2]; + Node dataNode = (Node) rrdData[3]; + + NodeList legendChildren = legend.getChildNodes(); + for (int col = 0; col < numColumns; col++) { + + if (legendChildren == null || legendChildren.item(col) == null) { + continue; + } + + String columnMetadata = getXMLNodeValue(legendChildren.item(col)); + + if (columnMetadata == null) { + continue; + } + + String[] columnMetadataList = columnMetadata.split(":"); + + if (columnMetadataList.length != 4) { + continue; + } + + String type = columnMetadataList[1]; + String param = columnMetadataList[3]; + + if (type.equalsIgnoreCase("host")) { + + if (param.contains("pif_eth0_rx")) { + hostStats.setNetworkReadKBs(getDataAverage(dataNode, col, numRows)); + } + + if (param.contains("pif_eth0_tx")) { + hostStats.setNetworkWriteKBs(getDataAverage(dataNode, col, numRows)); + } + + if (param.contains("memory_total_kib")) { + hostStats.setTotalMemoryKBs(getDataAverage(dataNode, col, numRows)); + } + + if (param.contains("memory_free_kib")) { + hostStats.setFreeMemoryKBs(getDataAverage(dataNode, col, numRows)); + } + + if (param.contains("cpu")) { + hostStats.setNumCpus(hostStats.getNumCpus() + 1); + hostStats.setCpuUtilization(hostStats.getCpuUtilization() + getDataAverage(dataNode, col, numRows)); + } + + if (param.contains("loadavg")) { + hostStats.setAverageLoad((hostStats.getAverageLoad() + getDataAverage(dataNode, col, numRows))); + } + } + } + + // add the host cpu utilization + if (hostStats.getNumCpus() != 0) { + hostStats.setCpuUtilization(hostStats.getCpuUtilization() / hostStats.getNumCpus()); + s_logger.debug("Host cpu utilization " + hostStats.getCpuUtilization()); + } + + return hostStats; + } + + protected GetVmStatsAnswer execute(GetVmStatsCommand cmd) { + List vmNames = cmd.getVmNames(); + HashMap vmStatsNameMap = new HashMap(); + if( vmNames.size() == 0 ) { + return new GetVmStatsAnswer(cmd, vmStatsNameMap); + } + + Connection conn = getConnection(); + try { + + // Determine the UUIDs of the requested VMs + List vmUUIDs = new ArrayList(); + + for (String vmName : vmNames) { + VM vm = getVM(conn, vmName); + vmUUIDs.add(vm.getUuid(conn)); + } + + HashMap vmStatsUUIDMap = getVmStats(cmd, vmUUIDs, cmd.getHostGuid()); + if( vmStatsUUIDMap == null ) + return new GetVmStatsAnswer(cmd, vmStatsNameMap); + + for (String vmUUID : vmStatsUUIDMap.keySet()) { + vmStatsNameMap.put(vmNames.get(vmUUIDs.indexOf(vmUUID)), vmStatsUUIDMap.get(vmUUID)); + } + + return new GetVmStatsAnswer(cmd, vmStatsNameMap); + } catch (XenAPIException e) { + String msg = "Unable to get VM stats" + e.toString(); + s_logger.warn(msg, e); + return new GetVmStatsAnswer(cmd, vmStatsNameMap); + } catch (XmlRpcException e) { + String msg = "Unable to get VM stats" + e.getMessage(); + s_logger.warn(msg, e); + return new GetVmStatsAnswer(cmd, vmStatsNameMap); + } + } + + protected HashMap getVmStats(GetVmStatsCommand cmd, List vmUUIDs, String hostGuid) { + HashMap vmResponseMap = new HashMap(); + + for (String vmUUID : vmUUIDs) { + vmResponseMap.put(vmUUID, new VmStatsEntry(0, 0, 0, 0, "vm")); + } + + Object[] rrdData = getRRDData(2); // call rrddata with 2 for vm + + if (rrdData == null) { + return null; + } + + Integer numRows = (Integer) rrdData[0]; + Integer numColumns = (Integer) rrdData[1]; + Node legend = (Node) rrdData[2]; + Node dataNode = (Node) rrdData[3]; + + NodeList legendChildren = legend.getChildNodes(); + for (int col = 0; col < numColumns; col++) { + + if (legendChildren == null || legendChildren.item(col) == null) { + continue; + } + + String columnMetadata = getXMLNodeValue(legendChildren.item(col)); + + if (columnMetadata == null) { + continue; + } + + String[] columnMetadataList = columnMetadata.split(":"); + + if (columnMetadataList.length != 4) { + continue; + } + + String type = columnMetadataList[1]; + String uuid = columnMetadataList[2]; + String param = columnMetadataList[3]; + + if (type.equals("vm") && vmResponseMap.keySet().contains(uuid)) { + VmStatsEntry vmStatsAnswer = vmResponseMap.get(uuid); + + vmStatsAnswer.setEntityType("vm"); + + if (param.contains("cpu")) { + vmStatsAnswer.setNumCPUs(vmStatsAnswer.getNumCPUs() + 1); + vmStatsAnswer.setCPUUtilization(vmStatsAnswer.getCPUUtilization() + getDataAverage(dataNode, col, numRows)); + } else if (param.equals("vif_0_rx")) { + vmStatsAnswer.setNetworkReadKBs(getDataAverage(dataNode, col, numRows)); + } else if (param.equals("vif_0_tx")) { + vmStatsAnswer.setNetworkWriteKBs(getDataAverage(dataNode, col, numRows)); + } + } + + } + + for (String vmUUID : vmResponseMap.keySet()) { + VmStatsEntry vmStatsAnswer = vmResponseMap.get(vmUUID); + + if (vmStatsAnswer.getNumCPUs() != 0) { + vmStatsAnswer.setCPUUtilization(vmStatsAnswer.getCPUUtilization() / vmStatsAnswer.getNumCPUs()); + s_logger.debug("Vm cpu utilization " + vmStatsAnswer.getCPUUtilization()); + } + } + + return vmResponseMap; + } + + protected Object[] getRRDData(int flag) { + + /* + * Note: 1 => called from host, hence host stats 2 => called from vm, hence vm stats + */ + String stats = ""; + + try { + if (flag == 1) + stats = getHostStatsRawXML(); + if (flag == 2) + stats = getVmStatsRawXML(); + } catch (Exception e1) { + s_logger.warn("Error whilst collecting raw stats from plugin:" + e1); + return null; + } + + // s_logger.debug("The raw xml stream is:"+stats); + // s_logger.debug("Length of raw xml is:"+stats.length()); + + //stats are null when the host plugin call fails (host down state) + if(stats == null) + return null; + + StringReader statsReader = new StringReader(stats); + InputSource statsSource = new InputSource(statsReader); + + Document doc = null; + try { + doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(statsSource); + } catch (Exception e) { + } + + NodeList firstLevelChildren = doc.getChildNodes(); + NodeList secondLevelChildren = (firstLevelChildren.item(0)).getChildNodes(); + Node metaNode = secondLevelChildren.item(0); + Node dataNode = secondLevelChildren.item(1); + + Integer numRows = 0; + Integer numColumns = 0; + Node legend = null; + NodeList metaNodeChildren = metaNode.getChildNodes(); + for (int i = 0; i < metaNodeChildren.getLength(); i++) { + Node n = metaNodeChildren.item(i); + if (n.getNodeName().equals("rows")) { + numRows = Integer.valueOf(getXMLNodeValue(n)); + } else if (n.getNodeName().equals("columns")) { + numColumns = Integer.valueOf(getXMLNodeValue(n)); + } else if (n.getNodeName().equals("legend")) { + legend = n; + } + } + + return new Object[] { numRows, numColumns, legend, dataNode }; + } + + protected String getXMLNodeValue(Node n) { + return n.getChildNodes().item(0).getNodeValue(); + } + + protected double getDataAverage(Node dataNode, int col, int numRows) { + double value = 0; + double dummy = 0; + int numRowsUsed = 0; + for (int row = 0; row < numRows; row++) { + Node data = dataNode.getChildNodes().item(numRows - 1 - row).getChildNodes().item(col + 1); + Double currentDataAsDouble = Double.valueOf(getXMLNodeValue(data)); + if (!currentDataAsDouble.equals(Double.NaN)) { + numRowsUsed += 1; + value += currentDataAsDouble; + } + } + + if(numRowsUsed == 0) + { + if((!Double.isInfinite(value))&&(!Double.isInfinite(value))) + { + return value; + } + else + { + s_logger.warn("Found an invalid value (infinity/NaN) in getDataAverage(), numRows=0"); + return dummy; + } + } + else + { + if((!Double.isInfinite(value/numRowsUsed))&&(!Double.isInfinite(value/numRowsUsed))) + { + return (value/numRowsUsed); + } + else + { + s_logger.warn("Found an invalid value (infinity/NaN) in getDataAverage(), numRows>0"); + return dummy; + } + } + + } + + protected String getHostStatsRawXML() { + Date currentDate = new Date(); + String startTime = String.valueOf(currentDate.getTime() / 1000 - 1000); + + return callHostPlugin("vmops", "gethostvmstats", "collectHostStats", String.valueOf("true"), "consolidationFunction", _consolidationFunction, "interval", String + .valueOf(_pollingIntervalInSeconds), "startTime", startTime); + } + + protected String getVmStatsRawXML() { + Date currentDate = new Date(); + String startTime = String.valueOf(currentDate.getTime() / 1000 - 1000); + + return callHostPlugin("vmops", "gethostvmstats", "collectHostStats", String.valueOf("false"), "consolidationFunction", _consolidationFunction, "interval", String + .valueOf(_pollingIntervalInSeconds), "startTime", startTime); + } + + protected void recordWarning(final VM vm, final String message, final Throwable e) { + Connection conn = getConnection(); + final StringBuilder msg = new StringBuilder(); + try { + final Long domId = vm.getDomid(conn); + msg.append("[").append(domId != null ? domId : -1l).append("] "); + } catch (final BadServerResponse e1) { + } catch (final XmlRpcException e1) { + } catch (XenAPIException e1) { + } + msg.append(message); + } + + protected State convertToState(Types.VmPowerState ps) { + final State state = s_statesTable.get(ps); + return state == null ? State.Unknown : state; + } + + protected HashMap getAllVms() { + final HashMap vmStates = new HashMap(); + Connection conn = getConnection(); + + Set vms = null; + for (int i = 0; i < 2; i++) { + try { + Host host = Host.getByUuid(conn, _host.uuid); + vms = host.getResidentVMs(conn); + break; + } catch (final Throwable e) { + s_logger.warn("Unable to get vms", e); + } + try { + Thread.sleep(1000); + } catch (final InterruptedException ex) { + + } + } + if (vms == null) { + return null; + } + for (VM vm : vms) { + VM.Record record = null; + for (int i = 0; i < 2; i++) { + try { + record = vm.getRecord(conn); + break; + } catch (XenAPIException e1) { + s_logger.debug("VM.getRecord failed on host:" + _host.uuid + " due to " + e1.toString()); + } catch (XmlRpcException e1) { + s_logger.debug("VM.getRecord failed on host:" + _host.uuid + " due to " + e1.getMessage()); + } + try { + Thread.sleep(1000); + } catch (final InterruptedException ex) { + + } + } + if (record == null) { + continue; + } + if (record.isControlDomain || record.isASnapshot || record.isATemplate) { + continue; // Skip DOM0 + } + + VmPowerState ps = record.powerState; + final State state = convertToState(ps); + if (s_logger.isTraceEnabled()) { + s_logger.trace("VM " + record.nameLabel + ": powerstate = " + ps + "; vm state=" + state.toString()); + } + vmStates.put(record.nameLabel, state); + } + + return vmStates; + } + + protected State getVmState(final String vmName) { + Connection conn = getConnection(); + int retry = 3; + while (retry-- > 0) { + try { + Set vms = VM.getByNameLabel(conn, vmName); + for (final VM vm : vms) { + return convertToState(vm.getPowerState(conn)); + } + } catch (final BadServerResponse e) { + // There is a race condition within xen such that if a vm is + // deleted and we + // happen to ask for it, it throws this stupid response. So + // if this happens, + // we take a nap and try again which then avoids the race + // condition because + // the vm's information is now cleaned up by xen. The error + // is as follows + // com.xensource.xenapi.Types$BadServerResponse + // [HANDLE_INVALID, VM, + // 3dde93f9-c1df-55a7-2cde-55e1dce431ab] + s_logger.info("Unable to get a vm PowerState due to " + e.toString() + ". We are retrying. Count: " + retry); + try { + Thread.sleep(3000); + } catch (final InterruptedException ex) { + + } + } catch (XenAPIException e) { + String msg = "Unable to get a vm PowerState due to " + e.toString(); + s_logger.warn(msg, e); + break; + } catch (final XmlRpcException e) { + String msg = "Unable to get a vm PowerState due to " + e.getMessage(); + s_logger.warn(msg, e); + break; + } + } + + return State.Stopped; + } + + protected CheckVirtualMachineAnswer execute(final CheckVirtualMachineCommand cmd) { + final String vmName = cmd.getVmName(); + final State state = getVmState(vmName); + Integer vncPort = null; + if (state == State.Running) { + synchronized (_vms) { + _vms.put(vmName, State.Running); + } + } + + return new CheckVirtualMachineAnswer(cmd, state, vncPort); + } + + protected PrepareForMigrationAnswer execute(final PrepareForMigrationCommand cmd) { + /* + * + * String result = null; + * + * List vols = cmd.getVolumes(); result = mountwithoutvdi(vols, cmd.getMappings()); if (result != + * null) { return new PrepareForMigrationAnswer(cmd, false, result); } + */ + final String vmName = cmd.getVmName(); + try { + + Connection conn = getConnection(); + Set hosts = Host.getAll(conn); + // workaround before implementing xenserver pool + // no migration + if (hosts.size() <= 1) { + return new PrepareForMigrationAnswer(cmd, false, "not in a same xenserver pool"); + } + // if the vm have CD + // 1. make iosSR shared + // 2. create pbd in target xenserver + SR sr = getISOSRbyVmName(cmd.getVmName()); + if (sr != null) { + Set pbds = sr.getPBDs(conn); + boolean found = false; + for (PBD pbd : pbds) { + if (Host.getByUuid(conn, _host.uuid).equals(pbd.getHost(conn))) { + found = true; + break; + } + } + if (!found) { + sr.setShared(conn, true); + PBD pbd = pbds.iterator().next(); + PBD.Record pbdr = new PBD.Record(); + pbdr.deviceConfig = pbd.getDeviceConfig(conn); + pbdr.host = Host.getByUuid(conn, _host.uuid); + pbdr.SR = sr; + PBD newpbd = PBD.create(conn, pbdr); + newpbd.plug(conn); + } + } + Set vms = VM.getByNameLabel(conn, vmName); + if (vms.size() != 1) { + String msg = "There are " + vms.size() + " " + vmName; + s_logger.warn(msg); + return new PrepareForMigrationAnswer(cmd, false, msg); + } + + VM vm = vms.iterator().next(); + + // check network + Set vifs = vm.getVIFs(conn); + for (VIF vif : vifs) { + Network network = vif.getNetwork(conn); + Set pifs = network.getPIFs(conn); + Long vlan = null; + PIF npif = null; + for (PIF pif : pifs) { + try { + vlan = pif.getVLAN(conn); + if (vlan != null) { + VLAN vland = pif.getVLANMasterOf(conn); + npif = vland.getTaggedPIF(conn); + break; + } + }catch (Exception e) { + vlan = null; + continue; + } + } + if (vlan == null) { + continue; + } + network = npif.getNetwork(conn); + String nwuuid = network.getUuid(conn); + + String pifuuid = null; + if(nwuuid.equalsIgnoreCase(_host.privateNetwork)) { + pifuuid = _host.privatePif; + } else if(nwuuid.equalsIgnoreCase(_host.publicNetwork)) { + pifuuid = _host.publicPif; + } else { + continue; + } + Network vlanNetwork = enableVlanNetwork(vlan, nwuuid, pifuuid); + + if (vlanNetwork == null) { + throw new InternalErrorException("Failed to enable VLAN network with tag: " + vlan); + } + } + + synchronized (_vms) { + _vms.put(cmd.getVmName(), State.Migrating); + } + return new PrepareForMigrationAnswer(cmd, true, null); + } catch (Exception e) { + String msg = "catch exception " + e.getMessage(); + s_logger.warn(msg, e); + return new PrepareForMigrationAnswer(cmd, false, msg); + } + } + + public DownloadAnswer execute(final PrimaryStorageDownloadCommand cmd) { + SR tmpltsr = null; + String tmplturl = cmd.getUrl(); + int index = tmplturl.lastIndexOf("/"); + String mountpoint = tmplturl.substring(0, index); + String tmpltname = null; + if (index < tmplturl.length() - 1) + tmpltname = tmplturl.substring(index + 1).replace(".vhd", ""); + try { + Connection conn = getConnection(); + String pUuid = cmd.getPoolUuid(); + SR poolsr = null; + Set srs = SR.getByNameLabel(conn, pUuid); + if (srs.size() != 1) { + String msg = "There are " + srs.size() + " SRs with same name: " + pUuid; + s_logger.warn(msg); + return new DownloadAnswer(null, 0, msg, com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR, "", "", 0); + } else { + poolsr = srs.iterator().next(); + } + + /* Does the template exist in primary storage pool? If yes, no copy */ + VDI vmtmpltvdi = null; + + Set vdis = VDI.getByNameLabel(conn, "Template " + cmd.getName()); + + for (VDI vdi : vdis) { + VDI.Record vdir = vdi.getRecord(conn); + if (vdir.SR.equals(poolsr)) { + vmtmpltvdi = vdi; + break; + } + } + String uuid; + if (vmtmpltvdi == null) { + tmpltsr = createNfsSRbyURI(new URI(mountpoint), false); + tmpltsr.scan(conn); + VDI tmpltvdi = null; + + if (tmpltname != null) { + tmpltvdi = getVDIbyUuid(tmpltname); + } + if (tmpltvdi == null) { + vdis = tmpltsr.getVDIs(conn); + for (VDI vdi : vdis) { + tmpltvdi = vdi; + break; + } + } + if (tmpltvdi == null) { + String msg = "Unable to find template vdi on secondary storage" + "host:" + _host.uuid + "pool: " + tmplturl; + s_logger.warn(msg); + return new DownloadAnswer(null, 0, msg, com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR, "", "", 0); + } + vmtmpltvdi = tmpltvdi.copy(conn, poolsr); + + vmtmpltvdi.setNameLabel(conn, "Template " + cmd.getName()); + // vmtmpltvdi.setNameDescription(conn, cmd.getDescription()); + uuid = vmtmpltvdi.getUuid(conn); + + } else + uuid = vmtmpltvdi.getUuid(conn); + + // Determine the size of the template + long createdSize = vmtmpltvdi.getVirtualSize(conn); + + DownloadAnswer answer = new DownloadAnswer(null, 100, cmd, com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOADED, uuid, uuid); + answer.setTemplateSize(createdSize); + + return answer; + + } catch (XenAPIException e) { + String msg = "XenAPIException:" + e.toString() + "host:" + _host.uuid + "pool: " + tmplturl; + s_logger.warn(msg, e); + return new DownloadAnswer(null, 0, msg, com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR, "", "", 0); + } catch (Exception e) { + String msg = "XenAPIException:" + e.getMessage() + "host:" + _host.uuid + "pool: " + tmplturl; + s_logger.warn(msg, e); + return new DownloadAnswer(null, 0, msg, com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR, "", "", 0); + } finally { + removeSR(tmpltsr); + } + + } + + protected String removeSRSync(SR sr) { + if (sr == null) { + return null; + } + if (s_logger.isDebugEnabled()) { + s_logger.debug(logX(sr, "Removing SR")); + } + + Connection conn = getConnection(); + long waittime = 0; + try { + Set vdis = sr.getVDIs(conn); + for (VDI vdi : vdis) { + Map currentOperation = vdi.getCurrentOperations(conn); + if (currentOperation == null || currentOperation.size() == 0) { + continue; + } + if (waittime >= 1800000) { + String msg = "This template is being used, try late time"; + s_logger.warn(msg); + return msg; + } + waittime += 30000; + try { + Thread.sleep(30000); + } catch (final InterruptedException ex) { + } + } + removeSR(sr); + return null; + } catch (XenAPIException e) { + s_logger.warn(logX(sr, "Unable to get current opertions " + e.toString()), e); + } catch (XmlRpcException e) { + s_logger.warn(logX(sr, "Unable to get current opertions " + e.getMessage()), e); + } + String msg = "Remove SR failed"; + s_logger.warn(msg); + return msg; + + } + + protected void removeSR(SR sr) { + if (sr == null) { + return; + } + if (s_logger.isDebugEnabled()) { + s_logger.debug(logX(sr, "Removing SR")); + } + + for (int i = 0; i < 2; i++) { + Connection conn = getConnection(); + try { + Set vdis = sr.getVDIs(conn); + for (VDI vdi : vdis) { + vdi.forget(conn); + } + Set pbds = sr.getPBDs(conn); + for (PBD pbd : pbds) { + if (s_logger.isDebugEnabled()) { + s_logger.debug(logX(pbd, "Unplugging pbd")); + } + if (pbd.getCurrentlyAttached(conn)) { + pbd.unplug(conn); + } + pbd.destroy(conn); + } + + pbds = sr.getPBDs(conn); + if (pbds.size() == 0) { + if (s_logger.isDebugEnabled()) { + s_logger.debug(logX(sr, "Forgetting")); + } + sr.forget(conn); + return; + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug(logX(sr, "There are still pbd attached")); + if (s_logger.isTraceEnabled()) { + for (PBD pbd : pbds) { + s_logger.trace(logX(pbd, " Still attached")); + } + } + } + } catch (XenAPIException e) { + s_logger.debug(logX(sr, "Catch XenAPIException: " + e.toString())); + } catch (XmlRpcException e) { + s_logger.debug(logX(sr, "Catch Exception: " + e.getMessage())); + } + } + s_logger.warn(logX(sr, "Unable to remove SR")); + } + + protected MigrateAnswer execute(final MigrateCommand cmd) { + final String vmName = cmd.getVmName(); + State state = null; + + synchronized (_vms) { + state = _vms.get(vmName); + _vms.put(vmName, State.Stopping); + } + try { + Connection conn = getConnection(); + Set vms = VM.getByNameLabel(conn, vmName); + + String ipaddr = cmd.getDestinationIp(); + + Set hosts = Host.getAll(conn); + Host dsthost = null; + for (Host host : hosts) { + if (host.getAddress(conn).equals(ipaddr)) { + dsthost = host; + break; + } + } + // if it is windows, we will not fake it is migrateable, + // windows requires PV driver to migrate + + for (VM vm : vms) { + if (!cmd.isWindows()) { + String uuid = vm.getUuid(conn); + String result = callHostPlugin("vmops", "preparemigration", "uuid", uuid); + if (result == null || result.isEmpty()) { + return new MigrateAnswer(cmd, false, "migration failed", null); + } + // check if pv version is successfully set up + int i = 0; + for (; i < 20; i++) { + try { + Thread.sleep(1000); + } catch (final InterruptedException ex) { + } + VMGuestMetrics vmmetric = vm.getGuestMetrics(conn); + + if (isRefNull(vmmetric)) + continue; + + Map PVversion = vmmetric.getPVDriversVersion(conn); + if (PVversion != null && PVversion.containsKey("major")) { + break; + } + + } + + if (i >= 20) { + String msg = "migration failed due to can not fake PV driver for " + vmName; + s_logger.warn(msg); + return new MigrateAnswer(cmd, false, msg, null); + } + } + final Map options = new HashMap(); + vm.poolMigrate(conn, dsthost, options); + state = State.Stopping; + + } + return new MigrateAnswer(cmd, true, "migration succeeded", null); + } catch (XenAPIException e) { + String msg = "migration failed due to " + e.toString(); + s_logger.warn(msg, e); + return new MigrateAnswer(cmd, false, msg, null); + } catch (XmlRpcException e) { + String msg = "migration failed due to " + e.getMessage(); + s_logger.warn(msg, e); + return new MigrateAnswer(cmd, false, msg, null); + } finally { + synchronized (_vms) { + _vms.put(vmName, state); + } + } + + } + + protected State getRealPowerState(String label) { + Connection conn = getConnection(); + int i = 0; + s_logger.trace("Checking on the HALTED State"); + for (; i < 20; i++) { + try { + Set vms = VM.getByNameLabel(conn, label); + if (vms == null || vms.size() == 0) { + continue; + } + + VM vm = vms.iterator().next(); + + VmPowerState vps = vm.getPowerState(conn); + if (vps != null && vps != VmPowerState.HALTED && vps != VmPowerState.UNKNOWN && vps != VmPowerState.UNRECOGNIZED) { + return convertToState(vps); + } + } catch (XenAPIException e) { + String msg = "Unable to get real power state due to " + e.toString(); + s_logger.warn(msg, e); + } catch (XmlRpcException e) { + String msg = "Unable to get real power state due to " + e.getMessage(); + s_logger.warn(msg, e); + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + } + return State.Stopped; + } + + protected Pair getControlDomain(Connection conn) throws XenAPIException, XmlRpcException { + Host host = Host.getByUuid(conn, _host.uuid); + Set vms = null; + vms = host.getResidentVMs(conn); + for (VM vm : vms) { + if (vm.getIsControlDomain(conn)) { + return new Pair(vm, vm.getRecord(conn)); + } + } + + throw new CloudRuntimeException("Com'on no control domain? What the crap?!#@!##$@"); + } + + protected HashMap sync() { + HashMap newStates; + HashMap oldStates = null; + + final HashMap changes = new HashMap(); + + synchronized (_vms) { + newStates = getAllVms(); + if (newStates == null) { + s_logger.debug("Unable to get the vm states so no state sync at this point."); + return null; + } + + oldStates = new HashMap(_vms.size()); + oldStates.putAll(_vms); + + for (final Map.Entry entry : newStates.entrySet()) { + final String vm = entry.getKey(); + + State newState = entry.getValue(); + final State oldState = oldStates.remove(vm); + + if (newState == State.Stopped && oldState != State.Stopping && oldState != null && oldState != State.Stopped) { + newState = getRealPowerState(vm); + } + + if (s_logger.isTraceEnabled()) { + s_logger.trace("VM " + vm + ": xen has state " + newState + " and we have state " + (oldState != null ? oldState.toString() : "null")); + } + + if (vm.startsWith("migrating")) { + s_logger.debug("Migrating from xen detected. Skipping"); + continue; + } + if (oldState == null) { + _vms.put(vm, newState); + s_logger.debug("Detecting a new state but couldn't find a old state so adding it to the changes: " + vm); + changes.put(vm, newState); + } else if (oldState == State.Starting) { + if (newState == State.Running) { + _vms.put(vm, newState); + } else if (newState == State.Stopped) { + s_logger.debug("Ignoring vm " + vm + " because of a lag in starting the vm."); + } + } else if (oldState == State.Migrating) { + if (newState == State.Running) { + s_logger.debug("Detected that an migrating VM is now running: " + vm); + _vms.put(vm, newState); + } + } else if (oldState == State.Stopping) { + if (newState == State.Stopped) { + _vms.put(vm, newState); + } else if (newState == State.Running) { + s_logger.debug("Ignoring vm " + vm + " because of a lag in stopping the vm. "); + } + } else if (oldState != newState) { + _vms.put(vm, newState); + if (newState == State.Stopped) { + /* + * if (_vmsKilled.remove(vm)) { s_logger.debug("VM " + vm + " has been killed for storage. "); + * newState = State.Error; } + */ + } + changes.put(vm, newState); + } + } + + for (final Map.Entry entry : oldStates.entrySet()) { + final String vm = entry.getKey(); + final State oldState = entry.getValue(); + + if (s_logger.isTraceEnabled()) { + s_logger.trace("VM " + vm + " is now missing from xen so reporting stopped"); + } + + if (oldState == State.Stopping) { + s_logger.debug("Ignoring VM " + vm + " in transition state stopping."); + _vms.remove(vm); + } else if (oldState == State.Starting) { + s_logger.debug("Ignoring VM " + vm + " in transition state starting."); + } else if (oldState == State.Stopped) { + _vms.remove(vm); + } else if (oldState == State.Migrating) { + s_logger.debug("Ignoring VM " + vm + " in migrating state."); + } else { + State state = State.Stopped; + /* + * if (_vmsKilled.remove(entry.getKey())) { s_logger.debug("VM " + vm + + * " has been killed by storage monitor"); state = State.Error; } + */ + changes.put(entry.getKey(), state); + } + } + } + + return changes; + } + + protected ReadyAnswer execute(ReadyCommand cmd) { + Long dcId = cmd.getDataCenterId(); + // Ignore the result of the callHostPlugin. Even if unmounting the + // snapshots dir fails, let Ready command + // succeed. + callHostPlugin("vmopsSnapshot", "unmountSnapshotsDir", "dcId", dcId.toString()); + return new ReadyAnswer(cmd); + } + + // + // using synchronized on VM name in the caller does not prevent multiple + // commands being sent against + // the same VM, there will be a race condition here in finally clause and + // the main block if + // there are multiple requests going on + // + // Therefore, a lazy solution is to add a synchronized guard here + protected int getVncPort(VM vm) { + Connection conn = getConnection(); + + VM.Record record; + try { + record = vm.getRecord(conn); + } catch (XenAPIException e) { + String msg = "Unable to get vnc-port due to " + e.toString(); + s_logger.warn(msg, e); + return -1; + } catch (XmlRpcException e) { + String msg = "Unable to get vnc-port due to " + e.getMessage(); + s_logger.warn(msg, e); + return -1; + } + String hvm = "true"; + if (record.HVMBootPolicy.isEmpty()) { + hvm = "false"; + } + + String vncport = callHostPlugin("vmops", "getvncport", "domID", record.domid.toString(), "hvm", hvm); + if (vncport == null || vncport.isEmpty()) { + return -1; + } + + vncport = vncport.replace("\n", ""); + return NumbersUtil.parseInt(vncport, -1); + } + + protected Answer execute(final RebootCommand cmd) { + + synchronized (_vms) { + _vms.put(cmd.getVmName(), State.Starting); + } + + try { + Connection conn = getConnection(); + Set vms = null; + try { + vms = VM.getByNameLabel(conn, cmd.getVmName()); + } catch (XenAPIException e0) { + s_logger.debug("getByNameLabel failed " + e0.toString()); + return new RebootAnswer(cmd, "getByNameLabel failed " + e0.toString()); + } catch (Exception e0) { + s_logger.debug("getByNameLabel failed " + e0.getMessage()); + return new RebootAnswer(cmd, "getByNameLabel failed"); + } + for (VM vm : vms) { + try { + vm.cleanReboot(conn); + } catch (XenAPIException e) { + s_logger.debug("Do Not support Clean Reboot, fall back to hard Reboot: " + e.toString()); + try { + vm.hardReboot(conn); + } catch (XenAPIException e1) { + s_logger.debug("Caught exception on hard Reboot " + e1.toString()); + return new RebootAnswer(cmd, "reboot failed: " + e1.toString()); + } catch (XmlRpcException e1) { + s_logger.debug("Caught exception on hard Reboot " + e1.getMessage()); + return new RebootAnswer(cmd, "reboot failed"); + } + } catch (XmlRpcException e) { + String msg = "Clean Reboot failed due to " + e.getMessage(); + s_logger.warn(msg, e); + return new RebootAnswer(cmd, msg); + } + } + return new RebootAnswer(cmd, "reboot succeeded", null, null); + } finally { + synchronized (_vms) { + _vms.put(cmd.getVmName(), State.Running); + } + } + } + + protected Answer execute(RebootRouterCommand cmd) { + Long bytesSent = 0L; + Long bytesRcvd = 0L; + if (VirtualMachineName.isValidRouterName(cmd.getVmName())) { + long[] stats = getNetworkStats(cmd.getPrivateIpAddress()); + bytesSent = stats[0]; + bytesRcvd = stats[1]; + } + RebootAnswer answer = (RebootAnswer) execute((RebootCommand) cmd); + answer.setBytesSent(bytesSent); + answer.setBytesReceived(bytesRcvd); + if (answer.getResult()) { + String cnct = connect(cmd.getVmName(), cmd.getPrivateIpAddress()); + networkUsage(cmd.getPrivateIpAddress(), "create", null); + if (cnct == null) { + return answer; + } else { + return new Answer(cmd, false, cnct); + } + } + return answer; + } + + protected VM createVmFromTemplate(Connection conn, StartCommand cmd) throws XenAPIException, XmlRpcException { + Set templates; + VM vm = null; + String stdType = cmd.getGuestOSDescription(); + String guestOsTypeName = getGuestOsType(stdType); + templates = VM.getByNameLabel(conn, guestOsTypeName); + assert templates.size() == 1 : "Should only have 1 template but found " + templates.size(); + VM template = templates.iterator().next(); + vm = template.createClone(conn, cmd.getVmName()); + vm.removeFromOtherConfig(conn, "disks"); + + if (!(guestOsTypeName.startsWith("Windows") || guestOsTypeName.startsWith("Citrix") || guestOsTypeName.startsWith("Other"))) { + if (cmd.getBootFromISO()) + vm.setPVBootloader(conn, "eliloader"); + else + vm.setPVBootloader(conn, "pygrub"); + + vm.addToOtherConfig(conn, "install-repository", "cdrom"); + } + return vm; + } + + protected String getGuestOsType(String stdType) { + return _guestOsType.get(stdType); + } + + public boolean joinPool(String address, String username, String password) { + Connection conn = getConnection(); + Connection poolConn = null; + try { + // set the _host.poolUuid to the old pool uuid in case it's not set. + _host.pool = getPoolUuid(); + + // Connect and find out about the new connection to the new pool. + poolConn = _connPool.connect(address, username, password, _wait); + Map pools = Pool.getAllRecords(poolConn); + Pool.Record pr = pools.values().iterator().next(); + + // Now join it. + String masterAddr = pr.master.getAddress(poolConn); + Pool.join(conn, masterAddr, username, password); + if (s_logger.isDebugEnabled()) { + s_logger.debug("Joined the pool at " + masterAddr); + } + disconnected(); // Logout of our own session. + try { + // slave will restart xapi in 10 sec + Thread.sleep(10000); + } catch (InterruptedException e) { + } + + // Set the pool uuid now to the newest pool. + _host.pool = pr.uuid; + URL url; + try { + url = new URL("http://" + _host.ip); + } catch (MalformedURLException e1) { + throw new CloudRuntimeException("Problem with url " + _host.ip); + } + Connection c = null; + for (int i = 0; i < 15; i++) { + c = new Connection(url, _wait); + try { + Session.loginWithPassword(c, _username, _password, APIVersion.latest().toString()); + s_logger.debug("Still waiting for the conversion to the master"); + Session.logout(c); + c.dispose(); + } catch (Types.HostIsSlave e) { + try { + Session.logout(c); + c.dispose(); + } catch (XmlRpcException e1) { + s_logger.debug("Unable to logout of test connection due to " + e1.getMessage()); + } catch (XenAPIException e1) { + s_logger.debug("Unable to logout of test connection due to " + e1.getMessage()); + } + break; + } catch (XmlRpcException e) { + s_logger.debug("XmlRpcException: Still waiting for the conversion to the master"); + } catch (Exception e) { + s_logger.debug("Exception: Still waiting for the conversion to the master"); + } + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + } + return true; + } catch (XenAPIException e) { + String msg = "Unable to allow host " + _host.uuid + " to join pool " + address + " due to " + e.toString(); + s_logger.warn(msg, e); + throw new RuntimeException(msg); + } catch (XmlRpcException e) { + String msg = "Unable to allow host " + _host.uuid + " to join pool " + address + " due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new RuntimeException(msg); + } finally { + if (poolConn != null) { + XenServerConnectionPool.logout(poolConn); + } + } + } + + protected void startvmfailhandle(VM vm, List> mounts) { + Connection conn = getConnection(); + + if (vm != null) { + try { + + if (vm.getPowerState(conn) == VmPowerState.RUNNING) { + try { + vm.hardShutdown(conn); + } catch (Exception e) { + String msg = "VM hardshutdown failed due to " + e.toString(); + s_logger.warn(msg); + } + } + if (vm.getPowerState(conn) == VmPowerState.HALTED) { + try { + vm.destroy(conn); + } catch (Exception e) { + String msg = "VM destroy failed due to " + e.toString(); + s_logger.warn(msg); + } + } + } catch (Exception e) { + String msg = "VM getPowerState failed due to " + e.toString(); + s_logger.warn(msg); + } + } + if (mounts != null) { + for (Ternary mount : mounts) { + VDI vdi = mount.second(); + Set vbds = null; + try { + vbds = vdi.getVBDs(conn); + } catch (Exception e) { + String msg = "VDI getVBDS failed due to " + e.toString(); + s_logger.warn(msg); + continue; + } + for (VBD vbd : vbds) { + try { + vbd.unplug(conn); + vbd.destroy(conn); + } catch (Exception e) { + String msg = "VBD destroy failed due to " + e.toString(); + s_logger.warn(msg); + } + } + } + } + } + + protected void setMemory(Connection conn, VM vm, long memsize) throws XmlRpcException, XenAPIException { + vm.setMemoryStaticMin(conn, memsize); + vm.setMemoryDynamicMin(conn, memsize); + + vm.setMemoryDynamicMax(conn, memsize); + vm.setMemoryStaticMax(conn, memsize); + } + + protected StartAnswer execute(StartCommand cmd) { + State state = State.Stopped; + Connection conn = getConnection(); + VM vm = null; + SR isosr = null; + List> mounts = null; + for (int retry = 0; retry < 2; retry++) { + try { + synchronized (_vms) { + _vms.put(cmd.getVmName(), State.Starting); + } + + List vols = cmd.getVolumes(); + + mounts = mount(vols); + if (retry == 1) { + // at the second time, try hvm + cmd.setGuestOSDescription("Other install media"); + } + + vm = createVmFromTemplate(conn, cmd); + + long memsize = cmd.getRamSize() * 1024L * 1024L; + setMemory(conn, vm, memsize); + + vm.setIsATemplate(conn, false); + + vm.setVCPUsMax(conn, (long) cmd.getCpu()); + vm.setVCPUsAtStartup(conn, (long) cmd.getCpu()); + + Host host = Host.getByUuid(conn, _host.uuid); + vm.setAffinity(conn, host); + + Map vcpuparam = new HashMap(); + + vcpuparam.put("weight", Integer.toString(cmd.getCpuWeight())); + vcpuparam.put("cap", Integer.toString(cmd.getUtilization())); + vm.setVCPUsParams(conn, vcpuparam); + + boolean bootFromISO = cmd.getBootFromISO(); + + /* create root VBD */ + VBD.Record vbdr = new VBD.Record(); + Ternary mount = mounts.get(0); + vbdr.VM = vm; + vbdr.VDI = mount.second(); + vbdr.bootable = !bootFromISO; + vbdr.userdevice = "0"; + vbdr.mode = Types.VbdMode.RW; + vbdr.type = Types.VbdType.DISK; + VBD.create(conn, vbdr); + + /* create data VBDs */ + for (int i = 1; i < mounts.size(); i++) { + mount = mounts.get(i); + // vdi.setNameLabel(conn, cmd.getVmName() + "-DATA"); + vbdr.VM = vm; + vbdr.VDI = mount.second(); + vbdr.bootable = false; + vbdr.userdevice = Long.toString(mount.third().getDeviceId()); + vbdr.mode = Types.VbdMode.RW; + vbdr.type = Types.VbdType.DISK; + vbdr.unpluggable = true; + VBD.create(conn, vbdr); + + } + + /* create CD-ROM VBD */ + VBD.Record cdromVBDR = new VBD.Record(); + cdromVBDR.VM = vm; + cdromVBDR.empty = true; + cdromVBDR.bootable = bootFromISO; + cdromVBDR.userdevice = "3"; + cdromVBDR.mode = Types.VbdMode.RO; + cdromVBDR.type = Types.VbdType.CD; + VBD cdromVBD = VBD.create(conn, cdromVBDR); + + /* insert the ISO VDI if isoPath is not null */ + String isopath = cmd.getISOPath(); + if (isopath != null) { + int index = isopath.lastIndexOf("/"); + + String mountpoint = isopath.substring(0, index); + URI uri = new URI(mountpoint); + isosr = createIsoSRbyURI(uri, cmd.getVmName(), false); + + String isoname = isopath.substring(index + 1); + + VDI isovdi = getVDIbyLocationandSR(isoname, isosr); + + if (isovdi == null) { + String msg = " can not find ISO " + cmd.getISOPath(); + s_logger.warn(msg); + return new StartAnswer(cmd, msg); + } else { + cdromVBD.insert(conn, isovdi); + } + + } + + createVIF(conn, vm, cmd.getGuestMacAddress(), cmd.getGuestNetworkId(), cmd.getNetworkRateMbps(), "0", false); + + if (cmd.getExternalMacAddress() != null && cmd.getExternalVlan() != null) { + createVIF(conn, vm, cmd.getExternalMacAddress(), cmd.getExternalVlan(), 0, "1", true); + } + + /* set action after crash as destroy */ + vm.setActionsAfterCrash(conn, Types.OnCrashBehaviour.DESTROY); + + vm.start(conn, false, true); + + if (_canBridgeFirewall) { + String result = callHostPlugin("vmops", "default_network_rules", + "vmName", cmd.getVmName(), + "vmIP", cmd.getGuestIpAddress(), + "vmMAC", cmd.getGuestMacAddress(), + "vmID", Long.toString(cmd.getId())); + if (result == null || result.isEmpty() || !Boolean.parseBoolean(result)) { + s_logger.warn("Failed to program default network rules for vm " + cmd.getVmName()); + } else { + s_logger.info("Programmed default network rules for vm " + cmd.getVmName()); + } + } + + state = State.Running; + return new StartAnswer(cmd); + + } catch (XenAPIException e) { + String errormsg = e.toString(); + String msg = "Exception caught while starting VM due to message:" + errormsg + " (" + e.getClass().getName() + ")"; + if (!errormsg.contains("Unable to find partition containing kernel") && !errormsg.contains("Unable to access a required file in the specified repository")) { + s_logger.warn(msg, e); + startvmfailhandle(vm, mounts); + removeSR(isosr); + } else { + startvmfailhandle(vm, mounts); + removeSR(isosr); + continue; + } + state = State.Stopped; + return new StartAnswer(cmd, msg); + } catch (Exception e) { + String msg = "Exception caught while starting VM due to message:" + e.getMessage(); + s_logger.warn(msg, e); + startvmfailhandle(vm, mounts); + removeSR(isosr); + state = State.Stopped; + return new StartAnswer(cmd, msg); + } finally { + synchronized (_vms) { + _vms.put(cmd.getVmName(), state); + } + + } + } + String msg = "Start VM failed"; + return new StartAnswer(cmd, msg); + } + + protected void createVIF(Connection conn, VM vm, String mac, String vlanTag, int rate, String devNum, boolean isPub) throws XenAPIException, XmlRpcException, + InternalErrorException { + VIF.Record vifr = new VIF.Record(); + vifr.VM = vm; + vifr.device = devNum; + vifr.MAC = mac; + + String nwUuid = (isPub ? _host.publicNetwork : _host.guestNetwork); + String pifUuid = (isPub ? _host.publicPif : _host.guestPif); + Network vlanNetwork = null; + if ("untagged".equalsIgnoreCase(vlanTag)) { + vlanNetwork = Network.getByUuid(conn, nwUuid); + } else { + vlanNetwork = enableVlanNetwork(Long.valueOf(vlanTag), nwUuid, pifUuid); + } + + if (vlanNetwork == null) { + throw new InternalErrorException("Failed to enable VLAN network with tag: " + vlanTag); + } + + vifr.network = vlanNetwork; + if (rate != 0) { + vifr.qosAlgorithmType = "ratelimit"; + vifr.qosAlgorithmParams = new HashMap(); + vifr.qosAlgorithmParams.put("kbps", Integer.toString(rate * 1000)); + } + + VIF.create(conn, vifr); + } + + protected StopAnswer execute(final StopCommand cmd) { + String vmName = cmd.getVmName(); + try { + Connection conn = getConnection(); + + Set vms = VM.getByNameLabel(conn, vmName); + // stop vm which is running on this host or is in halted state + for (VM vm : vms) { + VM.Record vmr = vm.getRecord(conn); + if (vmr.powerState != VmPowerState.RUNNING) + continue; + if (isRefNull(vmr.residentOn)) + continue; + if (vmr.residentOn.getUuid(conn).equals(_host.uuid)) + continue; + vms.remove(vm); + } + + if (vms.size() == 0) { + s_logger.warn("VM does not exist on XenServer" + _host.uuid); + synchronized (_vms) { + _vms.remove(vmName); + } + return new StopAnswer(cmd, "VM does not exist", 0, 0L, 0L); + } + Long bytesSent = 0L; + Long bytesRcvd = 0L; + for (VM vm : vms) { + VM.Record vmr = vm.getRecord(conn); + + if (vmr.isControlDomain) { + String msg = "Tring to Shutdown control domain"; + s_logger.warn(msg); + return new StopAnswer(cmd, msg); + } + + if (vmr.powerState == VmPowerState.RUNNING && !isRefNull(vmr.residentOn) && !vmr.residentOn.getUuid(conn).equals(_host.uuid)) { + String msg = "Stop Vm " + vmName + " failed due to this vm is not running on this host: " + _host.uuid + " but host:" + vmr.residentOn.getUuid(conn); + s_logger.warn(msg); + return new StopAnswer(cmd, msg); + } + + State state = null; + synchronized (_vms) { + state = _vms.get(vmName); + _vms.put(vmName, State.Stopping); + } + + try { + if (vmr.powerState == VmPowerState.RUNNING) { + /* when stop a vm, set affinity to current xenserver */ + vm.setAffinity(conn, vm.getResidentOn(conn)); + try { + if (VirtualMachineName.isValidRouterName(vmName)) { + if(cmd.getPrivateRouterIpAddress() != null){ + long[] stats = getNetworkStats(cmd.getPrivateRouterIpAddress()); + bytesSent = stats[0]; + bytesRcvd = stats[1]; + } + } + if (_canBridgeFirewall) { + String result = callHostPlugin("vmops", "destroy_network_rules_for_vm", "vmName", cmd.getVmName()); + if (result == null || result.isEmpty() || !Boolean.parseBoolean(result)) { + s_logger.warn("Failed to remove network rules for vm " + cmd.getVmName()); + } else { + s_logger.info("Removed network rules for vm " + cmd.getVmName()); + } + } + vm.cleanShutdown(conn); + + } catch (XenAPIException e) { + s_logger.debug("Do Not support Clean Shutdown, fall back to hard Shutdown: " + e.toString()); + try { + vm.hardShutdown(conn); + } catch (XenAPIException e1) { + String msg = "Hard Shutdown failed due to " + e1.toString(); + s_logger.warn(msg, e1); + return new StopAnswer(cmd, msg); + } catch (XmlRpcException e1) { + String msg = "Hard Shutdown failed due to " + e1.getMessage(); + s_logger.warn(msg, e1); + return new StopAnswer(cmd, msg); + } + } catch (XmlRpcException e) { + String msg = "Clean Shutdown failed due to " + e.getMessage(); + s_logger.warn(msg, e); + return new StopAnswer(cmd, msg); + } + } + } catch (Exception e) { + String msg = "Catch exception " + e.getClass().toString() + " when stop VM:" + cmd.getVmName(); + s_logger.debug(msg); + return new StopAnswer(cmd, msg); + } finally { + + try { + if (vm.getPowerState(conn) == VmPowerState.HALTED) { + Set vifs = vm.getVIFs(conn); + List networks = new ArrayList(); + for (VIF vif : vifs) { + networks.add(vif.getNetwork(conn)); + } + List vdis = getVdis(vm); + vm.destroy(conn); + for( VDI vdi : vdis ){ + umount(vdi); + } + state = State.Stopped; + SR sr = getISOSRbyVmName(cmd.getVmName()); + removeSR(sr); + // Disable any VLAN networks that aren't used + // anymore + for (Network network : networks) { + if (network.getNameLabel(conn).startsWith("VLAN")) { + disableVlanNetwork(network); + } + } + } + } catch (XenAPIException e) { + String msg = "VM destroy failed in Stop " + vmName + " Command due to " + e.toString(); + s_logger.warn(msg, e); + } catch (Exception e) { + String msg = "VM destroy failed in Stop " + vmName + " Command due to " + e.getMessage(); + s_logger.warn(msg, e); + } finally { + synchronized (_vms) { + _vms.put(vmName, state); + } + } + } + } + return new StopAnswer(cmd, "Stop VM " + vmName + " Succeed", 0, bytesSent, bytesRcvd); + } catch (XenAPIException e) { + String msg = "Stop Vm " + vmName + " fail due to " + e.toString(); + s_logger.warn(msg, e); + return new StopAnswer(cmd, msg); + } catch (XmlRpcException e) { + String msg = "Stop Vm " + vmName + " fail due to " + e.getMessage(); + s_logger.warn(msg, e); + return new StopAnswer(cmd, msg); + } + } + + private List getVdis(VM vm) { + List vdis = new ArrayList(); + try { + Connection conn = getConnection(); + Set vbds =vm.getVBDs(conn); + for( VBD vbd : vbds ) { + vdis.add(vbd.getVDI(conn)); + } + } catch (XenAPIException e) { + String msg = "getVdis can not get VPD due to " + e.toString(); + s_logger.warn(msg, e); + } catch (XmlRpcException e) { + String msg = "getVdis can not get VPD due to " + e.getMessage(); + s_logger.warn(msg, e); + } + return vdis; + } + + protected String connect(final String vmName, final String ipAddress, final int port) { + for (int i = 0; i <= _retry; i++) { + try { + Connection conn = getConnection(); + + Set vms = VM.getByNameLabel(conn, vmName); + if (vms.size() < 1) { + String msg = "VM " + vmName + " is not running"; + s_logger.warn(msg); + return msg; + } + } catch (Exception e) { + String msg = "VM.getByNameLabel " + vmName + " failed due to " + e.toString(); + s_logger.warn(msg, e); + return msg; + } + if (s_logger.isDebugEnabled()) { + s_logger.debug("Trying to connect to " + ipAddress); + } + if (pingdomr(ipAddress, Integer.toString(port))) + return null; + try { + Thread.sleep(_sleep); + } catch (final InterruptedException e) { + } + } + String msg = "Timeout, Unable to logon to " + ipAddress; + s_logger.debug(msg); + + return msg; + } + + protected String connect(final String vmname, final String ipAddress) { + return connect(vmname, ipAddress, 3922); + } + + protected StartRouterAnswer execute(StartRouterCommand cmd) { + final String vmName = cmd.getVmName(); + final DomainRouter router = cmd.getRouter(); + try { + String tag = router.getVnet(); + Network network = null; + if ("untagged".equalsIgnoreCase(tag)) { + Connection conn = getConnection(); + network = Network.getByUuid(conn, _host.guestNetwork); + } else { + network = enableVlanNetwork(Long.parseLong(tag), _host.guestNetwork, _host.guestPif); + } + + if (network == null) { + throw new InternalErrorException("Failed to enable VLAN network with tag: " + tag); + } + + String bootArgs = cmd.getBootArgs(); + + String result = startSystemVM(vmName, router.getVlanId(), network, cmd.getVolumes(), bootArgs, router.getGuestMacAddress(), router.getPrivateIpAddress(), router + .getPrivateMacAddress(), router.getPublicMacAddress(), 3922, router.getRamSize()); + if (result == null) { + networkUsage(router.getPrivateIpAddress(), "create", null); + return new StartRouterAnswer(cmd); + } + return new StartRouterAnswer(cmd, result); + + } catch (Exception e) { + String msg = "Exception caught while starting router vm " + vmName + " due to " + e.getMessage(); + s_logger.warn(msg, e); + return new StartRouterAnswer(cmd, msg); + } + } + + protected String startSystemVM(String vmName, String vlanId, Network nw0, List vols, String bootArgs, String guestMacAddr, String privateIp, String privateMacAddr, + String publicMacAddr, int cmdPort, long ramSize) { + + setupLinkLocalNetwork(); + VM vm = null; + List> mounts = null; + Connection conn = getConnection(); + State state = State.Stopped; + try { + synchronized (_vms) { + _vms.put(vmName, State.Starting); + } + + mounts = mount(vols); + + assert mounts.size() == 1 : "System VMs should have only 1 partition but we actually have " + mounts.size(); + + Ternary mount = mounts.get(0); + + if (!patchSystemVm(mount.second(), vmName)) { // FIXME make this + // nonspecific + String msg = "patch system vm failed"; + s_logger.warn(msg); + return msg; + } + + Set templates = VM.getByNameLabel(conn, "CentOS 5.3"); + if (templates.size() == 0) { + templates = VM.getByNameLabel(conn, "CentOS 5.3 (64-bit)"); + if (templates.size() == 0) { + String msg = " can not find template CentOS 5.3 "; + s_logger.warn(msg); + return msg; + } + } + + VM template = templates.iterator().next(); + + vm = template.createClone(conn, vmName); + + vm.removeFromOtherConfig(conn, "disks"); + + vm.setPVBootloader(conn, "pygrub"); + + long memsize = ramSize * 1024L * 1024L; + setMemory(conn, vm, memsize); + vm.setIsATemplate(conn, false); + + vm.setVCPUsAtStartup(conn, 1L); + + Host host = Host.getByUuid(conn, _host.uuid); + vm.setAffinity(conn, host); + + /* create VBD */ + VBD.Record vbdr = new VBD.Record(); + + vbdr.VM = vm; + vbdr.VDI = mount.second(); + vbdr.bootable = true; + vbdr.userdevice = "0"; + vbdr.mode = Types.VbdMode.RW; + vbdr.type = Types.VbdType.DISK; + VBD.create(conn, vbdr); + + + /* create VIF0 */ + VIF.Record vifr = new VIF.Record(); + vifr.VM = vm; + vifr.device = "0"; + vifr.MAC = guestMacAddr; + if (VirtualMachineName.isValidConsoleProxyName(vmName) || VirtualMachineName.isValidSecStorageVmName(vmName, null)) { + vifr.network = Network.getByUuid(conn, _host.linkLocalNetwork); + } else + vifr.network = nw0; + VIF.create(conn, vifr); + /* create VIF1 */ + /* For routing vm, set its network as link local bridge */ + vifr.VM = vm; + vifr.device = "1"; + vifr.MAC = privateMacAddr; + if (VirtualMachineName.isValidRouterName(vmName) && privateIp.startsWith("169.254")) { + vifr.network = Network.getByUuid(conn, _host.linkLocalNetwork); + } else { + vifr.network = Network.getByUuid(conn, _host.privateNetwork); + } + VIF.create(conn, vifr); + if( !publicMacAddr.equalsIgnoreCase("FE:FF:FF:FF:FF:FF") ) { + /* create VIF2 */ + vifr.VM = vm; + vifr.device = "2"; + vifr.MAC = publicMacAddr; + vifr.network = Network.getByUuid(conn, _host.publicNetwork); + if ("untagged".equalsIgnoreCase(vlanId)) { + vifr.network = Network.getByUuid(conn, _host.publicNetwork); + } else { + Network vlanNetwork = enableVlanNetwork(Long.valueOf(vlanId), _host.publicNetwork, _host.publicPif); + + if (vlanNetwork == null) { + throw new InternalErrorException("Failed to enable VLAN network with tag: " + vlanId); + } + + vifr.network = vlanNetwork; + } + VIF.create(conn, vifr); + } + /* set up PV dom argument */ + String pvargs = vm.getPVArgs(conn); + pvargs = pvargs + bootArgs; + + if (s_logger.isInfoEnabled()) + s_logger.info("PV args for system vm are " + pvargs); + vm.setPVArgs(conn, pvargs); + + /* destroy console */ + Set consoles = vm.getRecord(conn).consoles; + + for (Console console : consoles) { + console.destroy(conn); + } + + /* set action after crash as destroy */ + vm.setActionsAfterCrash(conn, Types.OnCrashBehaviour.DESTROY); + + vm.start(conn, false, true); + + if (_canBridgeFirewall) { + String result = callHostPlugin("vmops", "default_network_rules_systemvm", "vmName", vmName); + if (result == null || result.isEmpty() || !Boolean.parseBoolean(result)) { + s_logger.warn("Failed to program default system vm network rules for " + vmName); + } else { + s_logger.info("Programmed default system vm network rules for " + vmName); + } + } + + if (s_logger.isInfoEnabled()) + s_logger.info("Ping system vm command port, " + privateIp + ":" + cmdPort); + + state = State.Running; + String result = connect(vmName, privateIp, cmdPort); + if (result != null) { + String msg = "Can not ping System vm " + vmName + "due to:" + result; + s_logger.warn(msg); + throw new CloudRuntimeException(msg); + } else { + if (s_logger.isInfoEnabled()) + s_logger.info("Ping system vm command port succeeded for vm " + vmName); + } + return null; + + } catch (XenAPIException e) { + String msg = "Exception caught while starting System vm " + vmName + " due to " + e.toString(); + s_logger.warn(msg, e); + startvmfailhandle(vm, mounts); + state = State.Stopped; + return msg; + } catch (Exception e) { + String msg = "Exception caught while starting System vm " + vmName + " due to " + e.getMessage(); + s_logger.warn(msg, e); + startvmfailhandle(vm, mounts); + state = State.Stopped; + return msg; + } finally { + synchronized (_vms) { + _vms.put(vmName, state); + } + } + } + + // TODO : need to refactor it to reuse code with StartRouter + protected Answer execute(final StartConsoleProxyCommand cmd) { + final String vmName = cmd.getVmName(); + final ConsoleProxyVO proxy = cmd.getProxy(); + try { + Connection conn = getConnection(); + Network network = Network.getByUuid(conn, _host.privateNetwork); + String bootArgs = cmd.getBootArgs(); + bootArgs += " zone=" + _dcId; + bootArgs += " pod=" + _pod; + bootArgs += " guid=Proxy." + proxy.getId(); + bootArgs += " proxy_vm=" + proxy.getId(); + bootArgs += " localgw=" + _localGateway; + + String result = startSystemVM(vmName, proxy.getVlanId(), network, cmd.getVolumes(), bootArgs, proxy.getGuestMacAddress(), proxy.getGuestIpAddress(), proxy + .getPrivateMacAddress(), proxy.getPublicMacAddress(), cmd.getProxyCmdPort(), proxy.getRamSize()); + if (result == null) { + return new StartConsoleProxyAnswer(cmd); + } + return new StartConsoleProxyAnswer(cmd, result); + + } catch (Exception e) { + String msg = "Exception caught while starting router vm " + vmName + " due to " + e.getMessage(); + s_logger.warn(msg, e); + return new StartConsoleProxyAnswer(cmd, msg); + } + } + + protected boolean patchSystemVm(VDI vdi, String vmName) { + if (vmName.startsWith("r-")) { + return patchSpecialVM(vdi, vmName, "router"); + } else if (vmName.startsWith("v-")) { + return patchSpecialVM(vdi, vmName, "consoleproxy"); + } else if (vmName.startsWith("s-")) { + return patchSpecialVM(vdi, vmName, "secstorage"); + } else { + throw new CloudRuntimeException("Tried to patch unknown type of system vm"); + } + } + + protected boolean isDeviceUsed(VM vm, Long deviceId) { + // Figure out the disk number to attach the VM to + + String msg = null; + try { + Connection conn = getConnection(); + Set allowedVBDDevices = vm.getAllowedVBDDevices(conn); + if (allowedVBDDevices.contains(deviceId.toString())) { + return false; + } + return true; + } catch (XmlRpcException e) { + msg = "Catch XmlRpcException due to: " + e.getMessage(); + s_logger.warn(msg, e); + } catch (XenAPIException e) { + msg = "Catch XenAPIException due to: " + e.toString(); + s_logger.warn(msg, e); + } + throw new CloudRuntimeException("When check deviceId " + msg); + } + + + protected String getUnusedDeviceNum(VM vm) { + // Figure out the disk number to attach the VM to + try { + Connection conn = getConnection(); + Set allowedVBDDevices = vm.getAllowedVBDDevices(conn); + if (allowedVBDDevices.size() == 0) + throw new CloudRuntimeException("Could not find an available slot in VM with name: " + vm.getNameLabel(conn) + " to attach a new disk."); + return allowedVBDDevices.iterator().next(); + } catch (XmlRpcException e) { + String msg = "Catch XmlRpcException due to: " + e.getMessage(); + s_logger.warn(msg, e); + } catch (XenAPIException e) { + String msg = "Catch XenAPIException due to: " + e.toString(); + s_logger.warn(msg, e); + } + throw new CloudRuntimeException("Could not find an available slot in VM with name to attach a new disk."); + } + + protected boolean patchSpecialVM(VDI vdi, String vmname, String vmtype) { + // patch special vm here, domr, domp + VBD vbd = null; + Connection conn = getConnection(); + try { + Host host = Host.getByUuid(conn, _host.uuid); + + Set vms = host.getResidentVMs(conn); + + for (VM vm : vms) { + VM.Record vmrec = null; + try { + vmrec = vm.getRecord(conn); + } catch (Exception e) { + String msg = "VM.getRecord failed due to " + e.toString() + " " + e.getMessage(); + s_logger.warn(msg); + continue; + } + if (vmrec.isControlDomain) { + + /* create VBD */ + VBD.Record vbdr = new VBD.Record(); + vbdr.VM = vm; + vbdr.VDI = vdi; + vbdr.bootable = false; + vbdr.userdevice = getUnusedDeviceNum(vm); + vbdr.unpluggable = true; + vbdr.mode = Types.VbdMode.RW; + vbdr.type = Types.VbdType.DISK; + + vbd = VBD.create(conn, vbdr); + + vbd.plug(conn); + + String device = vbd.getDevice(conn); + + return patchspecialvm(vmname, device, vmtype); + } + } + + } catch (XenAPIException e) { + String msg = "patchSpecialVM faile on " + _host.uuid + " due to " + e.toString(); + s_logger.warn(msg, e); + } catch (Exception e) { + String msg = "patchSpecialVM faile on " + _host.uuid + " due to " + e.getMessage(); + s_logger.warn(msg, e); + } finally { + if (vbd != null) { + try { + if (vbd.getCurrentlyAttached(conn)) { + vbd.unplug(conn); + } + vbd.destroy(conn); + } catch (XmlRpcException e) { + String msg = "Catch XmlRpcException due to " + e.getMessage(); + s_logger.warn(msg, e); + } catch (XenAPIException e) { + String msg = "Catch XenAPIException due to " + e.toString(); + s_logger.warn(msg, e); + } + + } + } + return false; + } + + protected boolean patchspecialvm(String vmname, String device, String vmtype) { + String result = callHostPlugin("vmops", "patchdomr", "vmname", vmname, "vmtype", vmtype, "device", "/dev/" + device); + if (result == null || result.isEmpty()) + return false; + return true; + } + + protected String callHostPlugin(String plugin, String cmd, String... params) { + Map args = new HashMap(); + try { + Connection conn = getConnection(); + Host host = Host.getByUuid(conn, _host.uuid); + for (int i = 0; i < params.length; i += 2) { + args.put(params[i], params[i + 1]); + } + + if (s_logger.isTraceEnabled()) { + s_logger.trace("callHostPlugin executing for command " + cmd + " with " + getArgsString(args)); + } + + String result = host.callPlugin(conn, plugin, cmd, args); + if (s_logger.isTraceEnabled()) { + s_logger.trace("callHostPlugin Result: " + result); + } + return result.replace("\n", ""); + } catch (XenAPIException e) { + s_logger.warn("callHostPlugin failed for cmd: " + cmd + " with args " + getArgsString(args) + " due to " + e.toString()); + } catch (XmlRpcException e) { + s_logger.debug("callHostPlugin failed for cmd: " + cmd + " with args " + getArgsString(args) + " due to " + e.getMessage()); + } + return null; + } + + protected String getArgsString(Map args) { + StringBuilder argString = new StringBuilder(); + for (Map.Entry arg : args.entrySet()) { + argString.append(arg.getKey() + ": " + arg.getValue() + ", "); + } + return argString.toString(); + } + + protected boolean setIptables() { + String result = callHostPlugin("vmops", "setIptables"); + if (result == null || result.isEmpty()) + return false; + return true; + } + + protected Nic getLocalNetwork(Connection conn, String name) throws XmlRpcException, XenAPIException { + if( name == null) { + return null; + } + Set networks = Network.getByNameLabel(conn, name); + for (Network network : networks) { + Network.Record nr = network.getRecord(conn); + for (PIF pif : nr.PIFs) { + PIF.Record pr = pif.getRecord(conn); + if (_host.uuid.equals(pr.host.getUuid(conn))) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Found a network called " + name + " on host=" + _host.ip + "; Network=" + nr.uuid + "; pif=" + pr.uuid); + } + if (pr.bondMasterOf != null && pr.bondMasterOf.size() > 0) { + if (pr.bondMasterOf.size() > 1) { + String msg = new StringBuilder("Unsupported configuration. Network " + name + " has more than one bond. Network=").append(nr.uuid) + .append("; pif=").append(pr.uuid).toString(); + s_logger.warn(msg); + return null; + } + Bond bond = pr.bondMasterOf.iterator().next(); + Set slaves = bond.getSlaves(conn); + for (PIF slave : slaves) { + PIF.Record spr = slave.getRecord(conn); + if (spr.management) { + Host host = Host.getByUuid(conn, _host.uuid); + if (!transferManagementNetwork(conn, host, slave, spr, pif)) { + String msg = new StringBuilder("Unable to transfer management network. slave=" + spr.uuid + "; master=" + pr.uuid + "; host=" + + _host.uuid).toString(); + s_logger.warn(msg); + return null; + } + break; + } + } + } + + return new Nic(network, nr, pif, pr); + } + } + } + + return null; + } + + protected VIF getCorrectVif(VM router, String vlanId) { + try { + Connection conn = getConnection(); + Set routerVIFs = router.getVIFs(conn); + for (VIF vif : routerVIFs) { + Network vifNetwork = vif.getNetwork(conn); + if (vlanId.equals("untagged")) { + if (vifNetwork.getUuid(conn).equals(_host.publicNetwork)) { + return vif; + } + } else { + if (vifNetwork.getNameLabel(conn).equals("VLAN" + vlanId)) { + return vif; + } + } + } + } catch (XmlRpcException e) { + String msg = "Caught XmlRpcException: " + e.getMessage(); + s_logger.warn(msg, e); + } catch (XenAPIException e) { + String msg = "Caught XenAPIException: " + e.toString(); + s_logger.warn(msg, e); + } + + return null; + } + + protected String getLowestAvailableVIFDeviceNum(VM vm) { + try { + Connection conn = getConnection(); + Set availableDeviceNums = vm.getAllowedVIFDevices(conn); + Iterator deviceNumsIterator = availableDeviceNums.iterator(); + List sortedDeviceNums = new ArrayList(); + + while (deviceNumsIterator.hasNext()) { + try { + sortedDeviceNums.add(Integer.valueOf(deviceNumsIterator.next())); + } catch (NumberFormatException e) { + s_logger.debug("Obtained an invalid value for an available VIF device number for VM: " + vm.getNameLabel(conn)); + return null; + } + } + + Collections.sort(sortedDeviceNums); + return String.valueOf(sortedDeviceNums.get(0)); + } catch (XmlRpcException e) { + String msg = "Caught XmlRpcException: " + e.getMessage(); + s_logger.warn(msg, e); + } catch (XenAPIException e) { + String msg = "Caught XenAPIException: " + e.toString(); + s_logger.warn(msg, e); + } + + return null; + } + + protected VDI mount(StoragePoolType pooltype, String volumeFolder, String volumePath) { + return getVDIbyUuid(volumePath); + } + + protected List> mount(List vos) { + ArrayList> mounts = new ArrayList>(vos.size()); + + for (VolumeVO vol : vos) { + String vdiuuid = vol.getPath(); + SR sr = null; + VDI vdi = null; + // Look up the VDI + vdi = getVDIbyUuid(vdiuuid); + + Ternary ter = new Ternary(sr, vdi, vol); + if( vol.getVolumeType() == VolumeType.ROOT ) { + mounts.add(0, ter); + } else { + mounts.add(ter); + } + } + return mounts; + } + + protected Network getNetworkByName(String name) throws BadServerResponse, XenAPIException, XmlRpcException { + Connection conn = getConnection(); + + Set networks = Network.getByNameLabel(conn, name); + if (networks.size() > 0) { + assert networks.size() == 1 : "How did we find more than one network with this name label" + name + "? Strange...."; + return networks.iterator().next(); // Found it. + } + + return null; + } + + protected Network enableVlanNetwork(long tag, String networkUuid, String pifUuid) throws XenAPIException, XmlRpcException { + // In XenServer, vlan is added by + // 1. creating a network. + // 2. creating a vlan associating network with the pif. + // We always create + // 1. a network with VLAN[vlan id in decimal] + // 2. a vlan associating the network created with the pif to private + // network. + Connection conn = getConnection(); + + Network vlanNetwork = null; + String name = "VLAN" + Long.toString(tag); + + synchronized (name.intern()) { + vlanNetwork = getNetworkByName(name); + if (vlanNetwork == null) { // Can't find it, then create it. + if (s_logger.isDebugEnabled()) { + s_logger.debug("Creating VLAN network for " + tag + " on host " + _host.ip); + } + Network.Record nwr = new Network.Record(); + nwr.nameLabel = name; + nwr.bridge = name; + vlanNetwork = Network.create(conn, nwr); + } + + PIF nPif = PIF.getByUuid(conn, pifUuid); + PIF.Record nPifr = nPif.getRecord(conn); + + Network.Record vlanNetworkr = vlanNetwork.getRecord(conn); + if (vlanNetworkr.PIFs != null) { + for (PIF pif : vlanNetworkr.PIFs) { + PIF.Record pifr = pif.getRecord(conn); + if (pifr.device.equals(nPifr.device) && pifr.host.equals(nPifr.host)) { + pif.plug(conn); + return vlanNetwork; + } + } + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Creating VLAN " + tag + " on host " + _host.ip + " on device " + nPifr.device); + } + VLAN vlan = VLAN.create(conn, nPif, tag, vlanNetwork); + PIF untaggedPif = vlan.getUntaggedPIF(conn); + if (!untaggedPif.getCurrentlyAttached(conn)) { + untaggedPif.plug(conn); + } + } + + return vlanNetwork; + } + + protected void disableVlanNetwork(Network network) throws InternalErrorException { + try { + Connection conn = getConnection(); + if (network.getVIFs(conn).isEmpty()) { + Iterator pifs = network.getPIFs(conn).iterator(); + while (pifs.hasNext()) { + PIF pif = pifs.next(); + pif.unplug(conn); + } + } + } catch (XenAPIException e) { + String msg = "Unable to disable VLAN network due to " + e.toString(); + s_logger.warn(msg, e); + throw new InternalErrorException(msg); + } catch (XmlRpcException e) { + String msg = "Unable to disable VLAN network due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new InternalErrorException(msg); + } catch (Exception e) { + throw new InternalErrorException(e.getMessage()); + } + } + + protected SR getLocalLVMSR() { + Connection conn = getConnection(); + + try { + Map map = SR.getAllRecords(conn); + for (Map.Entry entry : map.entrySet()) { + SR.Record srRec = entry.getValue(); + if (SRType.LVM.equals(srRec.type)) { + Set pbds = srRec.PBDs; + if (pbds == null) { + continue; + } + for (PBD pbd : pbds) { + Host host = pbd.getHost(conn); + if (!isRefNull(host) && host.getUuid(conn).equals(_host.uuid)) { + if (!pbd.getCurrentlyAttached(conn)) { + pbd.plug(conn); + } + SR sr = entry.getKey(); + sr.scan(conn); + return sr; + } + } + } + } + } catch (XenAPIException e) { + String msg = "Unable to get local LVMSR in host:" + _host.uuid + e.toString(); + s_logger.warn(msg); + } catch (XmlRpcException e) { + String msg = "Unable to get local LVMSR in host:" + _host.uuid + e.getCause(); + s_logger.warn(msg); + } + return null; + + } + + protected StartupStorageCommand initializeLocalSR() { + + SR lvmsr = getLocalLVMSR(); + if (lvmsr == null) { + return null; + } + try { + Connection conn = getConnection(); + String lvmuuid = lvmsr.getUuid(conn); + long cap = lvmsr.getPhysicalSize(conn); + if (cap < 0) + return null; + long avail = cap - lvmsr.getPhysicalUtilisation(conn); + lvmsr.setNameLabel(conn, lvmuuid); + String name = "VMOps local storage pool in host : " + _host.uuid; + lvmsr.setNameDescription(conn, name); + Host host = Host.getByUuid(conn, _host.uuid); + String address = host.getAddress(conn); + StoragePoolInfo pInfo = new StoragePoolInfo(name, lvmuuid, address, SRType.LVM.toString(), SRType.LVM.toString(), StoragePoolType.LVM, cap, avail); + StartupStorageCommand cmd = new StartupStorageCommand(); + cmd.setPoolInfo(pInfo); + cmd.setGuid(_host.uuid); + cmd.setResourceType(StorageResourceType.STORAGE_POOL); + return cmd; + } catch (XenAPIException e) { + String msg = "build startupstoragecommand err in host:" + _host.uuid + e.toString(); + s_logger.warn(msg); + } catch (XmlRpcException e) { + String msg = "build startupstoragecommand err in host:" + _host.uuid + e.getMessage(); + s_logger.warn(msg); + } + return null; + + } + + @Override + public PingCommand getCurrentStatus(long id) { + try { + + if (!pingxenserver()) { + Thread.sleep(1000); + if (!pingxenserver()) { + s_logger.warn(" can not ping xenserver " + _host.uuid); + return null; + } + + } + + HashMap newStates = sync(); + if (newStates == null) { + newStates = new HashMap(); + } + if (!_canBridgeFirewall) { + return new PingRoutingCommand(getType(), id, newStates); + } else { + HashMap> nwGrpStates = syncNetworkGroups(id); + return new PingRoutingWithNwGroupsCommand(getType(), id, newStates, nwGrpStates); + } + } catch (Exception e) { + s_logger.warn("Unable to get current status", e); + return null; + } + } + + private HashMap> syncNetworkGroups(long id) { + HashMap> states = new HashMap>(); + + String result = callHostPlugin("vmops", "get_rule_logs_for_vms", "host_uuid", _host.uuid); + s_logger.trace("syncNetworkGroups: id=" + id + " got: " + result); + String [] rulelogs = result != null ?result.split(";"): new String [0]; + for (String rulesforvm: rulelogs){ + String [] log = rulesforvm.split(","); + if (log.length != 6) { + continue; + } + //output = ','.join([vmName, vmID, vmIP, domID, signature, seqno]) + try { + states.put(log[0], new Pair(Long.parseLong(log[1]), Long.parseLong(log[5]))); + } catch (NumberFormatException nfe) { + states.put(log[0], new Pair(-1L, -1L)); + } + } + return states; + } + + @Override + public Type getType() { + return com.cloud.host.Host.Type.Routing; + } + + protected void getPVISO(StartupStorageCommand sscmd) { + Connection conn = getConnection(); + try { + Set vids = VDI.getByNameLabel(conn, "xs-tools.iso"); + if (vids.isEmpty()) + return; + VDI pvISO = vids.iterator().next(); + String uuid = pvISO.getUuid(conn); + Map pvISOtmlt = new HashMap(); + TemplateInfo tmplt = new TemplateInfo("xs-tools.iso", uuid, pvISO.getVirtualSize(conn), true); + pvISOtmlt.put("xs-tools", tmplt); + sscmd.setTemplateInfo(pvISOtmlt); + } catch (XenAPIException e) { + s_logger.debug("Can't get xs-tools.iso: " + e.toString()); + } catch (XmlRpcException e) { + s_logger.debug("Can't get xs-tools.iso: " + e.toString()); + } + } + + protected boolean getHostInfo() throws IllegalArgumentException{ + Connection conn = getConnection(); + + try { + Host myself = Host.getByUuid(conn, _host.uuid); + _host.pool = getPoolUuid(); + + String name = "cloud-private"; + if (_privateNetworkName != null) { + name = _privateNetworkName; + } + + _localGateway = callHostPlugin("vmops", "getgateway", "mgmtIP", myself.getAddress(conn)); + if (_localGateway == null || _localGateway.isEmpty()) { + s_logger.warn("can not get gateway for host :" + _host.uuid); + return false; + } + + _canBridgeFirewall = Boolean.valueOf(callHostPlugin("vmops", "can_bridge_firewall", "host_uuid", _host.uuid)); + + Nic privateNic = getLocalNetwork(conn, name); + if (privateNic == null) { + s_logger.debug("Unable to find any private network. Trying to determine that by route for host " + _host.ip); + name = callHostPlugin("vmops", "getnetwork", "mgmtIP", myself.getAddress(conn)); + if (name == null || name.isEmpty()) { + s_logger.warn("Unable to determine the private network for host " + _host.ip); + return false; + } + privateNic = getLocalNetwork(conn, name); + if (privateNic == null) { + s_logger.warn("Unable to get private network " + name); + return false; + } + } + _host.privatePif = privateNic.pr.uuid; + _host.privateNetwork = privateNic.nr.uuid; + + Nic guestNic = null; + if (_guestNetworkName != null && !_guestNetworkName.equals(_privateNetworkName)) { + guestNic = getLocalNetwork(conn, _guestNetworkName); + if (guestNic == null) { + s_logger.warn("Unable to find guest network " + _guestNetworkName); + throw new IllegalArgumentException("Unable to find guest network " + _guestNetworkName + " for host " + _host.ip); + } + } else { + guestNic = privateNic; + } + _host.guestNetwork = guestNic.nr.uuid; + _host.guestPif = guestNic.pr.uuid; + + Nic publicNic = null; + if (_publicNetworkName != null && !_publicNetworkName.equals(_guestNetworkName)) { + publicNic = getLocalNetwork(conn, _publicNetworkName); + if (publicNic == null) { + s_logger.warn("Unable to find public network " + _publicNetworkName + " for host " + _host.ip); + throw new IllegalArgumentException("Unable to find public network " + _publicNetworkName + " for host " + _host.ip); + } + } else { + publicNic = guestNic; + } + _host.publicPif = publicNic.pr.uuid; + _host.publicNetwork = publicNic.nr.uuid; + + + Nic storageNic1 = getLocalNetwork(conn, _storageNetworkName1); + if (storageNic1 == null) { + storageNic1 = privateNic; + _storageNetworkName1 = _privateNetworkName; + } + _host.storageNetwork1 = storageNic1.nr.uuid; + _host.storagePif1 = storageNic1.pr.uuid; + + Nic storageNic2 = getLocalNetwork(conn, _storageNetworkName2); + if (storageNic2 == null) { + storageNic2 = privateNic; + _storageNetworkName2 = _privateNetworkName; + } + _host.storageNetwork2 = storageNic2.nr.uuid; + _host.storagePif2 = storageNic2.pr.uuid; + + s_logger.info("Private Network is " + _privateNetworkName + " for host " + _host.ip); + s_logger.info("Public Network is " + _publicNetworkName + " for host " + _host.ip); + s_logger.info("Storage Network 1 is " + _storageNetworkName1 + " for host " + _host.ip); + s_logger.info("Storage Network 2 is " + _storageNetworkName2 + " for host " + _host.ip); + + return true; + } catch (XenAPIException e) { + s_logger.warn("Unable to get host information for " + _host.ip, e); + return false; + } catch (XmlRpcException e) { + s_logger.warn("Unable to get host information for " + _host.ip, e); + return false; + } + } + + private void setupLinkLocalNetwork() { + try { + Network.Record rec = new Network.Record(); + Connection conn = getConnection(); + Set networks = Network.getByNameLabel(conn, _linkLocalPrivateNetworkName); + Network linkLocal = null; + + if (networks.size() == 0) { + rec.nameDescription = "link local network used by system vms"; + rec.nameLabel = _linkLocalPrivateNetworkName; + Map configs = new HashMap(); + configs.put("ip_begin", NetUtils.getLinkLocalGateway()); + configs.put("ip_end", NetUtils.getLinkLocalIpEnd()); + configs.put("netmask", NetUtils.getLinkLocalNetMask()); + rec.otherConfig = configs; + linkLocal = Network.create(conn, rec); + + } else { + linkLocal = networks.iterator().next(); + } + + /* Make sure there is a physical bridge on this network */ + VIF dom0vif = null; + Pair vm = getControlDomain(conn); + VM dom0 = vm.first(); + Set vifs = dom0.getVIFs(conn); + if (vifs.size() != 0) { + for (VIF vif : vifs) { + Map otherConfig = vif.getOtherConfig(conn); + if (otherConfig != null) { + String nameLabel = otherConfig.get("nameLabel"); + if ((nameLabel != null) && nameLabel.equalsIgnoreCase("link_local_network_vif")) { + dom0vif = vif; + } + } + } + } + + /* create temp VIF0 */ + if (dom0vif == null) { + s_logger.debug("Can't find a vif on dom0 for link local, creating a new one"); + VIF.Record vifr = new VIF.Record(); + vifr.VM = dom0; + vifr.device = getLowestAvailableVIFDeviceNum(dom0); + if (vifr.device == null) { + s_logger.debug("Failed to create link local network, no vif available"); + return; + } + Map config = new HashMap(); + config.put("nameLabel", "link_local_network_vif"); + vifr.otherConfig = config; + vifr.MAC = "FE:FF:FF:FF:FF:FF"; + vifr.network = linkLocal; + dom0vif = VIF.create(conn, vifr); + dom0vif.plug(conn); + } else { + s_logger.debug("already have a vif on dom0 for link local network"); + if (!dom0vif.getCurrentlyAttached(conn)) { + dom0vif.plug(conn); + } + } + + String brName = linkLocal.getBridge(conn); + callHostPlugin("vmops", "setLinkLocalIP", "brName", brName); + _host.linkLocalNetwork = linkLocal.getUuid(conn); + + } catch (XenAPIException e) { + s_logger.warn("Unable to create local link network", e); + } catch (XmlRpcException e) { + // TODO Auto-generated catch block + s_logger.warn("Unable to create local link network", e); + } + } + + protected boolean transferManagementNetwork(Connection conn, Host host, PIF src, PIF.Record spr, PIF dest) throws XmlRpcException, XenAPIException { + dest.reconfigureIp(conn, spr.ipConfigurationMode, spr.IP, spr.netmask, spr.gateway, spr.DNS); + Host.managementReconfigure(conn, dest); + String hostUuid = null; + int count = 0; + while (count < 10) { + try { + Thread.sleep(10000); + hostUuid = host.getUuid(conn); + if (hostUuid != null) { + break; + } + } catch (XmlRpcException e) { + s_logger.debug("Waiting for host to come back: " + e.getMessage()); + } catch (XenAPIException e) { + s_logger.debug("Waiting for host to come back: " + e.getMessage()); + } catch (InterruptedException e) { + s_logger.debug("Gotta run"); + return false; + } + } + if (hostUuid == null) { + s_logger.warn("Unable to transfer the management network from " + spr.uuid); + return false; + } + + src.reconfigureIp(conn, IpConfigurationMode.NONE, null, null, null, null); + return true; + } + + @Override + public StartupCommand[] initialize() throws IllegalArgumentException{ + disconnected(); + + setupServer(); + + if (!getHostInfo()) { + s_logger.warn("Unable to get host information for " + _host.ip); + return null; + } + + destroyStoppedVm(); + StartupRoutingCommand cmd = new StartupRoutingCommand(); + fillHostInfo(cmd); + + cleanupDiskMounts(); + + Map changes = null; + synchronized (_vms) { + _vms.clear(); + changes = sync(); + } + + cmd.setHypervisorType(Hypervisor.Type.XenServer); + cmd.setChanges(changes); + cmd.setCluster(_cluster); + + StartupStorageCommand sscmd = initializeLocalSR(); + + _host.pool = getPoolUuid(); + + if (sscmd != null) { + /* report pv driver iso */ + getPVISO(sscmd); + return new StartupCommand[] { cmd, sscmd }; + } + + return new StartupCommand[] { cmd }; + } + + protected String getPoolUuid() { + Connection conn = getConnection(); + try { + Map pools = Pool.getAllRecords(conn); + assert (pools.size() == 1) : "Tell me how pool size can be " + pools.size(); + Pool.Record rec = pools.values().iterator().next(); + return rec.uuid; + } catch (XenAPIException e) { + throw new CloudRuntimeException("Unable to get pool ", e); + } catch (XmlRpcException e) { + throw new CloudRuntimeException("Unable to get pool ", e); + } + } + + protected void setupServer() { + Connection conn = getConnection(); + + String version = CitrixResourceBase.class.getPackage().getImplementationVersion(); + + try { + Host host = Host.getByUuid(conn, _host.uuid); + /* enable host in case it is disabled somehow */ + host.enable(conn); + /* push patches to XenServer */ + Host.Record hr = host.getRecord(conn); + + Iterator it = hr.tags.iterator(); + + while (it.hasNext()) { + String tag = it.next(); + if (tag.startsWith("vmops-version-")) { + if (tag.contains(version)) { + s_logger.info(logX(host, "Host " + hr.address + " is already setup.")); + return; + } else { + it.remove(); + } + } + } + + com.trilead.ssh2.Connection sshConnection = new com.trilead.ssh2.Connection(hr.address, 22); + try { + sshConnection.connect(null, 60000, 60000); + if (!sshConnection.authenticateWithPassword(_username, _password)) { + throw new CloudRuntimeException("Unable to authenticate"); + } + + SCPClient scp = new SCPClient(sshConnection); + File file = new File(_patchPath); + + Properties props = new Properties(); + props.load(new FileInputStream(file)); + + String path = _patchPath.substring(0, _patchPath.lastIndexOf(File.separator) + 1); + for (Map.Entry entry : props.entrySet()) { + String k = (String) entry.getKey(); + String v = (String) entry.getValue(); + + assert (k != null && k.length() > 0 && v != null && v.length() > 0) : "Problems with " + k + "=" + v; + + String[] tokens = v.split(","); + String f = null; + if (tokens.length == 3 && tokens[0].length() > 0) { + if (tokens[0].startsWith("/")) { + f = tokens[0]; + } else if (tokens[0].startsWith("~")) { + String homedir = System.getenv("HOME"); + f = homedir + tokens[0].substring(1) + k; + } else { + f = path + tokens[0] + '/' + k; + } + } else { + f = path + k; + } + String d = tokens[tokens.length - 1]; + f = f.replace('/', File.separatorChar); + + String p = "0755"; + if (tokens.length == 3) { + p = tokens[1]; + } else if (tokens.length == 2) { + p = tokens[0]; + } + + if (!new File(f).exists()) { + s_logger.warn("We cannot locate " + f); + continue; + } + if (s_logger.isDebugEnabled()) { + s_logger.debug("Copying " + f + " to " + d + " on " + hr.address + " with permission " + p); + } + scp.put(f, d, p); + + } + } catch (IOException e) { + throw new CloudRuntimeException("Unable to setup the server correctly", e); + } finally { + sshConnection.close(); + } + try { + // wait 2 seconds before call plugin + Thread.sleep(2000); + } catch (final InterruptedException ex) { + + } + if (!setIptables()) { + s_logger.warn("set xenserver Iptable failed"); + } + + hr.tags.add("vmops-version-" + version); + host.setTags(conn, hr.tags); + } catch (XenAPIException e) { + String msg = "Xen setup failed due to " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException("Unable to get host information " + e.toString(), e); + } catch (XmlRpcException e) { + String msg = "Xen setup failed due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new CloudRuntimeException("Unable to get host information ", e); + } + } + + protected SR getSRByNameLabelandHost(String name) throws BadServerResponse, XenAPIException, XmlRpcException { + Connection conn = getConnection(); + Set srs = SR.getByNameLabel(conn, name); + SR ressr = null; + for (SR sr : srs) { + Set pbds; + pbds = sr.getPBDs(conn); + for (PBD pbd : pbds) { + PBD.Record pbdr = pbd.getRecord(conn); + if (pbdr.host != null && pbdr.host.getUuid(conn).equals(_host.uuid)) { + if (!pbdr.currentlyAttached) { + pbd.plug(conn); + } + ressr = sr; + break; + } + } + } + return ressr; + } + + protected GetStorageStatsAnswer execute(final GetStorageStatsCommand cmd) { + + try { + Connection conn = getConnection(); + Set srs = SR.getByNameLabel(conn, cmd.getStorageId()); + + if (srs.size() != 1) { + String msg = "There are " + srs.size() + " storageid: " + cmd.getStorageId(); + s_logger.warn(msg); + return new GetStorageStatsAnswer(cmd, msg); + } + + SR sr = srs.iterator().next(); + + sr.scan(conn); + long capacity = sr.getPhysicalSize(conn); + long used = sr.getPhysicalUtilisation(conn); + return new GetStorageStatsAnswer(cmd, capacity, used); + } catch (XenAPIException e) { + String msg = "GetStorageStats Exception:" + e.toString() + "host:" + _host.uuid + "storageid: " + cmd.getStorageId(); + s_logger.warn(msg); + return new GetStorageStatsAnswer(cmd, msg); + } catch (XmlRpcException e) { + String msg = "GetStorageStats Exception:" + e.getMessage() + "host:" + _host.uuid + "storageid: " + cmd.getStorageId(); + s_logger.warn(msg); + return new GetStorageStatsAnswer(cmd, msg); + } + } + + protected boolean checkSR(SR sr) { + + try { + Connection conn = getConnection(); + SR.Record srr = sr.getRecord(conn); + Set pbds = sr.getPBDs(conn); + if (pbds.size() == 0) { + String msg = "There is no PBDs for this SR: " + _host.uuid; + s_logger.warn(msg); + removeSR(sr); + return false; + } + Set hosts = null; + if (srr.shared) { + hosts = Host.getAll(conn); + + for (Host host : hosts) { + boolean found = false; + for (PBD pbd : pbds) { + if (host.equals(pbd.getHost(conn))) { + PBD.Record pbdr = pbd.getRecord(conn); + if (currentlyAttached(sr, srr, pbd, pbdr)) { + if (!pbdr.currentlyAttached) { + pbd.plug(conn); + } + } else { + if (pbdr.currentlyAttached) { + pbd.unplug(conn); + } + pbd.plug(conn); + } + pbds.remove(pbd); + found = true; + break; + } + } + if (!found) { + PBD.Record pbdr = srr.PBDs.iterator().next().getRecord(conn); + pbdr.host = host; + pbdr.uuid = ""; + PBD pbd = PBD.create(conn, pbdr); + pbd.plug(conn); + } + } + } else { + for (PBD pbd : pbds) { + PBD.Record pbdr = pbd.getRecord(conn); + if (!pbdr.currentlyAttached) { + pbd.plug(conn); + } + } + } + + } catch (Exception e) { + String msg = "checkSR failed host:" + _host.uuid; + s_logger.warn(msg); + return false; + } + return true; + } + + protected Answer execute(ModifyStoragePoolCommand cmd) { + StoragePoolVO pool = cmd.getPool(); + try { + Connection conn = getConnection(); + + SR sr = getStorageRepository(conn, pool); + if (!checkSR(sr)) { + String msg = "ModifyStoragePoolCommand checkSR failed! host:" + _host.uuid + " pool: " + pool.getName() + pool.getHostAddress() + pool.getPath(); + s_logger.warn(msg); + return new Answer(cmd, false, msg); + } + sr.setNameLabel(conn, pool.getUuid()); + sr.setNameDescription(conn, pool.getName()); + long capacity = sr.getPhysicalSize(conn); + long available = capacity - sr.getPhysicalUtilisation(conn); + if (capacity == -1) { + String msg = "Pool capacity is -1! pool: " + pool.getName() + pool.getHostAddress() + pool.getPath(); + s_logger.warn(msg); + return new Answer(cmd, false, msg); + } + Map tInfo = new HashMap(); + ModifyStoragePoolAnswer answer = new ModifyStoragePoolAnswer(cmd, capacity, available, tInfo); + return answer; + } catch (XenAPIException e) { + String msg = "ModifyStoragePoolCommand XenAPIException:" + e.toString() + " host:" + _host.uuid + " pool: " + pool.getName() + pool.getHostAddress() + pool.getPath(); + s_logger.warn(msg, e); + return new Answer(cmd, false, msg); + } catch (Exception e) { + String msg = "ModifyStoragePoolCommand XenAPIException:" + e.getMessage() + " host:" + _host.uuid + " pool: " + pool.getName() + pool.getHostAddress() + pool.getPath(); + s_logger.warn(msg, e); + return new Answer(cmd, false, msg); + } + + } + + protected Answer execute(DeleteStoragePoolCommand cmd) { + StoragePoolVO pool = cmd.getPool(); + try { + Connection conn = getConnection(); + SR sr = getStorageRepository(conn, pool); + if (!checkSR(sr)) { + String msg = "DeleteStoragePoolCommand checkSR failed! host:" + _host.uuid + " pool: " + pool.getName() + pool.getHostAddress() + pool.getPath(); + s_logger.warn(msg); + return new Answer(cmd, false, msg); + } + sr.setNameLabel(conn, pool.getUuid()); + sr.setNameDescription(conn, pool.getName()); + + Answer answer = new Answer(cmd, true, "success"); + return answer; + } catch (XenAPIException e) { + String msg = "DeleteStoragePoolCommand XenAPIException:" + e.toString() + " host:" + _host.uuid + " pool: " + pool.getName() + pool.getHostAddress() + pool.getPath(); + s_logger.warn(msg, e); + return new Answer(cmd, false, msg); + } catch (Exception e) { + String msg = "DeleteStoragePoolCommand XenAPIException:" + e.getMessage() + " host:" + _host.uuid + " pool: " + pool.getName() + pool.getHostAddress() + pool.getPath(); + s_logger.warn(msg, e); + return new Answer(cmd, false, msg); + } + + } + + public Connection getConnection() { + return _connPool.connect(_host.uuid, _host.ip, _username, _password, _wait); + } + + protected void fillHostInfo(StartupRoutingCommand cmd) { + long speed = 0; + int cpus = 0; + long ram = 0; + + Connection conn = getConnection(); + + long dom0Ram = 0; + final StringBuilder caps = new StringBuilder(); + try { + + Host host = Host.getByUuid(conn, _host.uuid); + Host.Record hr = host.getRecord(conn); + + Map details = cmd.getHostDetails(); + if (details == null) { + details = new HashMap(); + } + if (_privateNetworkName != null) { + details.put("private.network.device", _privateNetworkName); + } + if (_publicNetworkName != null) { + details.put("public.network.device", _publicNetworkName); + } + if (_guestNetworkName != null) { + details.put("guest.network.device", _guestNetworkName); + } + details.put("can_bridge_firewall", Boolean.toString(_canBridgeFirewall)); + cmd.setHostDetails(details); + cmd.setName(hr.nameLabel); + cmd.setGuid(_host.uuid); + cmd.setDataCenter(Long.toString(_dcId)); + for (final String cap : hr.capabilities) { + if (cap.length() > 0) { + caps.append(cap).append(" , "); + } + } + if (caps.length() > 0) { + caps.delete(caps.length() - 3, caps.length()); + } + cmd.setCaps(caps.toString()); + + Set hcs = host.getHostCPUs(conn); + cpus = hcs.size(); + for (final HostCpu hc : hcs) { + speed = hc.getSpeed(conn); + } + cmd.setSpeed(speed); + cmd.setCpus(cpus); + + long free = 0; + + HostMetrics hm = host.getMetrics(conn); + + ram = hm.getMemoryTotal(conn); + free = hm.getMemoryFree(conn); + + Set vms = host.getResidentVMs(conn); + for (VM vm : vms) { + if (vm.getIsControlDomain(conn)) { + dom0Ram = vm.getMemoryDynamicMax(conn); + break; + } + } + // assume the memory Virtualization overhead is 1/64 + ram = (ram - dom0Ram) * 63/64; + cmd.setMemory(ram); + cmd.setDom0MinMemory(dom0Ram); + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Total Ram: " + ram + " Free Ram: " + free + " dom0 Ram: " + dom0Ram); + } + + PIF pif = PIF.getByUuid(conn, _host.privatePif); + PIF.Record pifr = pif.getRecord(conn); + if (pifr.IP != null && pifr.IP.length() > 0) { + cmd.setPrivateIpAddress(pifr.IP); + cmd.setPrivateMacAddress(pifr.MAC); + cmd.setPrivateNetmask(pifr.netmask); + } + + pif = PIF.getByUuid(conn, _host.storagePif1); + pifr = pif.getRecord(conn); + if (pifr.IP != null && pifr.IP.length() > 0) { + cmd.setStorageIpAddress(pifr.IP); + cmd.setStorageMacAddress(pifr.MAC); + cmd.setStorageNetmask(pifr.netmask); + } + + if (_host.storagePif2 != null) { + pif = PIF.getByUuid(conn, _host.storagePif2); + pifr = pif.getRecord(conn); + if (pifr.IP != null && pifr.IP.length() > 0) { + cmd.setStorageIpAddressDeux(pifr.IP); + cmd.setStorageMacAddressDeux(pifr.MAC); + cmd.setStorageNetmaskDeux(pifr.netmask); + } + } + + Map configs = hr.otherConfig; + cmd.setIqn(configs.get("iscsi_iqn")); + + cmd.setPod(_pod); + cmd.setVersion(CitrixResourceBase.class.getPackage().getImplementationVersion()); + + } catch (final XmlRpcException e) { + throw new CloudRuntimeException("XML RPC Exception" + e.getMessage(), e); + } catch (XenAPIException e) { + throw new CloudRuntimeException("XenAPIException" + e.toString(), e); + } + } + + public CitrixResourceBase() { + } + + protected String getPatchPath() { + return "scripts/vm/hypervisor/xenserver/xcpserver"; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + _name = name; + _host.uuid = (String) params.get("guid"); + try { + _dcId = Long.parseLong((String) params.get("zone")); + } catch (NumberFormatException e) { + throw new ConfigurationException("Unable to get the zone " + params.get("zone")); + } + _name = _host.uuid; + _host.ip = (String) params.get("url"); + _username = (String) params.get("username"); + _password = (String) params.get("password"); + _pod = (String) params.get("pod"); + _cluster = (String)params.get("cluster"); + _privateNetworkName = (String) params.get("private.network.device"); + _publicNetworkName = (String) params.get("public.network.device"); + _guestNetworkName = (String)params.get("guest.network.device"); + + _linkLocalPrivateNetworkName = (String) params.get("private.linkLocal.device"); + if (_linkLocalPrivateNetworkName == null) + _linkLocalPrivateNetworkName = "cloud_link_local_network"; + + _storageNetworkName1 = (String) params.get("storage.network.device1"); + if (_storageNetworkName1 == null) { + _storageNetworkName1 = "cloud-stor1"; + } + _storageNetworkName2 = (String) params.get("storage.network.device2"); + if (_storageNetworkName2 == null) { + _storageNetworkName2 = "cloud-stor2"; + } + + String value = (String) params.get("wait"); + _wait = NumbersUtil.parseInt(value, 1800); + + if (_pod == null) { + throw new ConfigurationException("Unable to get the pod"); + } + + if (_host.ip == null) { + throw new ConfigurationException("Unable to get the host address"); + } + + if (_username == null) { + throw new ConfigurationException("Unable to get the username"); + } + + if (_password == null) { + throw new ConfigurationException("Unable to get the password"); + } + + if (_host.uuid == null) { + throw new ConfigurationException("Unable to get the uuid"); + } + + params.put("domr.scripts.dir", "scripts/network/domr"); + + String patchPath = getPatchPath(); + + _patchPath = Script.findScript(patchPath, "patch"); + if (_patchPath == null) { + throw new ConfigurationException("Unable to find all of patch files for xenserver"); + } + + _storage = (StorageLayer) params.get(StorageLayer.InstanceConfigKey); + if (_storage == null) { + value = (String) params.get(StorageLayer.ClassConfigKey); + if (value == null) { + value = "com.cloud.storage.JavaStorageLayer"; + } + + try { + Class clazz = Class.forName(value); + _storage = (StorageLayer) ComponentLocator.inject(clazz); + _storage.configure("StorageLayer", params); + } catch (ClassNotFoundException e) { + throw new ConfigurationException("Unable to find class " + value); + } + } + + return true; + } + + void destroyVDI(VDI vdi) { + try { + Connection conn = getConnection(); + vdi.destroy(conn); + + } catch (Exception e) { + String msg = "destroy VDI failed due to " + e.toString(); + s_logger.warn(msg); + } + } + + public CreateAnswer execute(CreateCommand cmd) { + StoragePoolTO pool = cmd.getPool(); + DiskCharacteristicsTO dskch = cmd.getDiskCharacteristics(); + + VDI vdi = null; + Connection conn = getConnection(); + try { + SR poolSr = getStorageRepository(conn, pool); + + if (cmd.getTemplateUrl() != null) { + VDI tmpltvdi = null; + + tmpltvdi = getVDIbyUuid(cmd.getTemplateUrl()); + vdi = tmpltvdi.createClone(conn, new HashMap()); + vdi.setNameLabel(conn, dskch.getName()); + } else { + VDI.Record vdir = new VDI.Record(); + vdir.nameLabel = dskch.getName(); + vdir.SR = poolSr; + vdir.type = Types.VdiType.USER; + + if(cmd.getSize()!=0) + vdir.virtualSize = cmd.getSize(); + else + vdir.virtualSize = dskch.getSize(); + vdi = VDI.create(conn, vdir); + } + + VDI.Record vdir; + vdir = vdi.getRecord(conn); + s_logger.debug("Succesfully created VDI for " + cmd + ". Uuid = " + vdir.uuid); + + VolumeTO vol = new VolumeTO(cmd.getVolumeId(), dskch.getType(), StorageResourceType.STORAGE_POOL, pool.getType(), vdir.nameLabel, pool.getPath(), vdir.uuid, + vdir.virtualSize); + return new CreateAnswer(cmd, vol); + } catch (Exception e) { + s_logger.warn("Unable to create volume; Pool=" + pool + "; Disk: " + dskch, e); + return new CreateAnswer(cmd, e); + } + } + + protected SR getISOSRbyVmName(String vmName) { + + Connection conn = getConnection(); + try { + Set srs = SR.getByNameLabel(conn, vmName + "-ISO"); + if (srs.size() == 0) { + return null; + } else if (srs.size() == 1) { + return srs.iterator().next(); + } else { + String msg = "getIsoSRbyVmName failed due to there are more than 1 SR having same Label"; + s_logger.warn(msg); + } + } catch (XenAPIException e) { + String msg = "getIsoSRbyVmName failed due to " + e.toString(); + s_logger.warn(msg, e); + } catch (Exception e) { + String msg = "getIsoSRbyVmName failed due to " + e.getMessage(); + s_logger.warn(msg, e); + } + return null; + } + + protected SR createNfsSRbyURI(URI uri, boolean shared) { + try { + Connection conn = getConnection(); + if (s_logger.isDebugEnabled()) { + s_logger.debug("Creating a " + (shared ? "shared SR for " : "not shared SR for ") + uri); + } + + Map deviceConfig = new HashMap(); + String path = uri.getPath(); + path = path.replace("//", "/"); + deviceConfig.put("server", uri.getHost()); + deviceConfig.put("serverpath", path); + String name = UUID.nameUUIDFromBytes(new String(uri.getHost() + path).getBytes()).toString(); + if (!shared) { + Set srs = SR.getByNameLabel(conn, name); + for (SR sr : srs) { + SR.Record record = sr.getRecord(conn); + if (SRType.NFS.equals(record.type) && record.contentType.equals("user") && !record.shared) { + removeSRSync(sr); + } + } + } + + Host host = Host.getByUuid(conn, _host.uuid); + + SR sr = SR.create(conn, host, deviceConfig, new Long(0), name, uri.getHost() + uri.getPath(), SRType.NFS.toString(), "user", shared, new HashMap()); + if( !checkSR(sr) ) { + throw new Exception("no attached PBD"); + } + if (s_logger.isDebugEnabled()) { + s_logger.debug(logX(sr, "Created a SR; UUID is " + sr.getUuid(conn))); + } + sr.scan(conn); + return sr; + } catch (XenAPIException e) { + String msg = "Can not create second storage SR mountpoint: " + uri.getHost() + uri.getPath() + " due to " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } catch (Exception e) { + String msg = "Can not create second storage SR mountpoint: " + uri.getHost() + uri.getPath() + " due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } + } + + protected SR createIsoSRbyURI(URI uri, String vmName, boolean shared) { + try { + Connection conn = getConnection(); + + Map deviceConfig = new HashMap(); + String path = uri.getPath(); + path = path.replace("//", "/"); + deviceConfig.put("location", uri.getHost() + ":" + uri.getPath()); + Host host = Host.getByUuid(conn, _host.uuid); + SR sr = SR.create(conn, host, deviceConfig, new Long(0), uri.getHost() + uri.getPath(), "iso", "iso", "iso", shared, new HashMap()); + sr.setNameLabel(conn, vmName + "-ISO"); + sr.setNameDescription(conn, deviceConfig.get("location")); + + sr.scan(conn); + return sr; + } catch (XenAPIException e) { + String msg = "createIsoSRbyURI failed! mountpoint: " + uri.getHost() + uri.getPath() + " due to " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } catch (Exception e) { + String msg = "createIsoSRbyURI failed! mountpoint: " + uri.getHost() + uri.getPath() + " due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } + } + + protected VDI getVDIbyLocationandSR(String loc, SR sr) { + Connection conn = getConnection(); + try { + Set vdis = sr.getVDIs(conn); + for (VDI vdi : vdis) { + if (vdi.getLocation(conn).startsWith(loc)) { + return vdi; + } + } + + String msg = "can not getVDIbyLocationandSR " + loc; + s_logger.warn(msg); + return null; + } catch (XenAPIException e) { + String msg = "getVDIbyLocationandSR exception " + loc + " due to " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } catch (Exception e) { + String msg = "getVDIbyLocationandSR exception " + loc + " due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } + + } + + protected VDI getVDIbyUuid(String uuid) { + try { + Connection conn = getConnection(); + return VDI.getByUuid(conn, uuid); + } catch (XenAPIException e) { + String msg = "VDI getByUuid for uuid: " + uuid + " failed due to " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } catch (Exception e) { + String msg = "VDI getByUuid for uuid: " + uuid + " failed due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } + + } + + protected SR getIscsiSR(Connection conn, StoragePoolVO pool) { + + synchronized (pool.getUuid().intern()) { + Map deviceConfig = new HashMap(); + try { + String target = pool.getHostAddress().trim(); + String path = pool.getPath().trim(); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + String tmp[] = path.split("/"); + if (tmp.length != 3) { + String msg = "Wrong iscsi path " + pool.getPath() + " it should be /targetIQN/LUN"; + s_logger.warn(msg); + throw new CloudRuntimeException(msg); + } + String targetiqn = tmp[1].trim(); + String lunid = tmp[2].trim(); + String scsiid = ""; + + Set srs = SR.getByNameLabel(conn, pool.getUuid()); + for (SR sr : srs) { + if (!SRType.LVMOISCSI.equals(sr.getType(conn))) + continue; + + Set pbds = sr.getPBDs(conn); + if (pbds.isEmpty()) + continue; + + PBD pbd = pbds.iterator().next(); + + Map dc = pbd.getDeviceConfig(conn); + + if (dc == null) + continue; + + if (dc.get("target") == null) + continue; + + if (dc.get("targetIQN") == null) + continue; + + if (dc.get("lunid") == null) + continue; + + if (target.equals(dc.get("target")) && targetiqn.equals(dc.get("targetIQN")) && lunid.equals(dc.get("lunid"))) { + return sr; + } + + } + deviceConfig.put("target", target); + deviceConfig.put("targetIQN", targetiqn); + + Host host = Host.getByUuid(conn, _host.uuid); + SR sr = null; + try { + sr = SR.create(conn, host, deviceConfig, new Long(0), pool.getUuid(), pool.getName(), SRType.LVMOISCSI.toString(), "user", true, new HashMap()); + } catch (XenAPIException e) { + String errmsg = e.toString(); + if (errmsg.contains("SR_BACKEND_FAILURE_107")) { + String lun[] = errmsg.split(""); + boolean found = false; + for (int i = 1; i < lun.length; i++) { + int blunindex = lun[i].indexOf("") + 7; + int elunindex = lun[i].indexOf(""); + String ilun = lun[i].substring(blunindex, elunindex); + ilun = ilun.trim(); + if (ilun.equals(lunid)) { + int bscsiindex = lun[i].indexOf("") + 8; + int escsiindex = lun[i].indexOf(""); + scsiid = lun[i].substring(bscsiindex, escsiindex); + scsiid = scsiid.trim(); + found = true; + break; + } + } + if (!found) { + String msg = "can not find LUN " + lunid + " in " + errmsg; + s_logger.warn(msg); + throw new CloudRuntimeException(msg); + } + } else { + String msg = "Unable to create Iscsi SR " + deviceConfig + " due to " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } + } + deviceConfig.put("SCSIid", scsiid); + sr = SR.create(conn, host, deviceConfig, new Long(0), pool.getUuid(), pool.getName(), SRType.LVMOISCSI.toString(), "user", true, new HashMap()); + if( !checkSR(sr) ) { + throw new Exception("no attached PBD"); + } + sr.scan(conn); + return sr; + + } catch (XenAPIException e) { + String msg = "Unable to create Iscsi SR " + deviceConfig + " due to " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } catch (Exception e) { + String msg = "Unable to create Iscsi SR " + deviceConfig + " due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } + } + } + + protected SR getIscsiSR(Connection conn, StoragePoolTO pool) { + + synchronized (pool.getUuid().intern()) { + Map deviceConfig = new HashMap(); + try { + String target = pool.getHost(); + String path = pool.getPath(); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + String tmp[] = path.split("/"); + if (tmp.length != 3) { + String msg = "Wrong iscsi path " + pool.getPath() + " it should be /targetIQN/LUN"; + s_logger.warn(msg); + throw new CloudRuntimeException(msg); + } + String targetiqn = tmp[1].trim(); + String lunid = tmp[2].trim(); + String scsiid = ""; + + Set srs = SR.getByNameLabel(conn, pool.getUuid()); + for (SR sr : srs) { + if (!SRType.LVMOISCSI.equals(sr.getType(conn))) + continue; + + Set pbds = sr.getPBDs(conn); + if (pbds.isEmpty()) + continue; + + PBD pbd = pbds.iterator().next(); + + Map dc = pbd.getDeviceConfig(conn); + + if (dc == null) + continue; + + if (dc.get("target") == null) + continue; + + if (dc.get("targetIQN") == null) + continue; + + if (dc.get("lunid") == null) + continue; + + if (target.equals(dc.get("target")) && targetiqn.equals(dc.get("targetIQN")) && lunid.equals(dc.get("lunid"))) { + if (checkSR(sr)) { + return sr; + } + } + + } + deviceConfig.put("target", target); + deviceConfig.put("targetIQN", targetiqn); + + Host host = Host.getByUuid(conn, _host.uuid); + SR sr = null; + try { + sr = SR.create(conn, host, deviceConfig, new Long(0), pool.getUuid(), Long.toString(pool.getId()), SRType.LVMOISCSI.toString(), "user", true, + new HashMap()); + } catch (XenAPIException e) { + String errmsg = e.toString(); + if (errmsg.contains("SR_BACKEND_FAILURE_107")) { + String lun[] = errmsg.split(""); + boolean found = false; + for (int i = 1; i < lun.length; i++) { + int blunindex = lun[i].indexOf("") + 7; + int elunindex = lun[i].indexOf(""); + String ilun = lun[i].substring(blunindex, elunindex); + ilun = ilun.trim(); + if (ilun.equals(lunid)) { + int bscsiindex = lun[i].indexOf("") + 8; + int escsiindex = lun[i].indexOf(""); + scsiid = lun[i].substring(bscsiindex, escsiindex); + scsiid = scsiid.trim(); + found = true; + break; + } + } + if (!found) { + String msg = "can not find LUN " + lunid + " in " + errmsg; + s_logger.warn(msg); + throw new CloudRuntimeException(msg); + } + } else { + String msg = "Unable to create Iscsi SR " + deviceConfig + " due to " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } + } + deviceConfig.put("SCSIid", scsiid); + sr = SR.create(conn, host, deviceConfig, new Long(0), pool.getUuid(), Long.toString(pool.getId()), SRType.LVMOISCSI.toString(), "user", true, + new HashMap()); + sr.scan(conn); + return sr; + + } catch (XenAPIException e) { + String msg = "Unable to create Iscsi SR " + deviceConfig + " due to " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } catch (Exception e) { + String msg = "Unable to create Iscsi SR " + deviceConfig + " due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } + } + } + + protected SR getNfsSR(StoragePoolVO pool) { + Connection conn = getConnection(); + + Map deviceConfig = new HashMap(); + try { + + String server = pool.getHostAddress(); + String serverpath = pool.getPath(); + serverpath = serverpath.replace("//", "/"); + Set srs = SR.getAll(conn); + for (SR sr : srs) { + if (!SRType.NFS.equals(sr.getType(conn))) + continue; + + Set pbds = sr.getPBDs(conn); + if (pbds.isEmpty()) + continue; + + PBD pbd = pbds.iterator().next(); + + Map dc = pbd.getDeviceConfig(conn); + + if (dc == null) + continue; + + if (dc.get("server") == null) + continue; + + if (dc.get("serverpath") == null) + continue; + + if (server.equals(dc.get("server")) && serverpath.equals(dc.get("serverpath"))) { + if (checkSR(sr)) { + return sr; + } + } + + } + + deviceConfig.put("server", server); + deviceConfig.put("serverpath", serverpath); + Host host = Host.getByUuid(conn, _host.uuid); + SR sr = SR.create(conn, host, deviceConfig, new Long(0), pool.getUuid(), pool.getName(), SRType.NFS.toString(), "user", true, new HashMap()); + sr.scan(conn); + return sr; + + } catch (XenAPIException e) { + String msg = "Unable to create NFS SR " + deviceConfig + " due to " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } catch (Exception e) { + String msg = "Unable to create NFS SR " + deviceConfig + " due to " + e.getMessage(); + s_logger.warn(msg); + throw new CloudRuntimeException(msg, e); + } + } + + protected SR getNfsSR(Connection conn, StoragePoolTO pool) { + Map deviceConfig = new HashMap(); + + String server = pool.getHost(); + String serverpath = pool.getPath(); + serverpath = serverpath.replace("//", "/"); + try { + Set srs = SR.getAll(conn); + for (SR sr : srs) { + if (!SRType.NFS.equals(sr.getType(conn))) + continue; + + Set pbds = sr.getPBDs(conn); + if (pbds.isEmpty()) + continue; + + PBD pbd = pbds.iterator().next(); + + Map dc = pbd.getDeviceConfig(conn); + + if (dc == null) + continue; + + if (dc.get("server") == null) + continue; + + if (dc.get("serverpath") == null) + continue; + + if (server.equals(dc.get("server")) && serverpath.equals(dc.get("serverpath"))) { + return sr; + } + + } + + deviceConfig.put("server", server); + deviceConfig.put("serverpath", serverpath); + Host host = Host.getByUuid(conn, _host.uuid); + SR sr = SR.create(conn, host, deviceConfig, new Long(0), pool.getUuid(), Long.toString(pool.getId()), SRType.NFS.toString(), "user", true, + new HashMap()); + sr.scan(conn); + return sr; + } catch (XenAPIException e) { + throw new CloudRuntimeException("Unable to create NFS SR " + pool.toString(), e); + } catch (XmlRpcException e) { + throw new CloudRuntimeException("Unable to create NFS SR " + pool.toString(), e); + } + } + + public Answer execute(DestroyCommand cmd) { + VolumeTO vol = cmd.getVolume(); + + Connection conn = getConnection(); + // Look up the VDI + String volumeUUID = vol.getPath(); + VDI vdi = null; + try { + vdi = getVDIbyUuid(volumeUUID); + } catch (Exception e) { + String msg = "getVDIbyUuid for " + volumeUUID + " failed due to " + e.toString(); + s_logger.warn(msg); + return new Answer(cmd, true, "Success"); + } + Set vbds = null; + try { + vbds = vdi.getVBDs(conn); + } catch (Exception e) { + String msg = "VDI getVBDS for " + volumeUUID + " failed due to " + e.toString(); + s_logger.warn(msg, e); + return new Answer(cmd, false, msg); + } + for (VBD vbd : vbds) { + try { + vbd.unplug(conn); + vbd.destroy(conn); + } catch (Exception e) { + String msg = "VM destroy for " + volumeUUID + " failed due to " + e.toString(); + s_logger.warn(msg, e); + return new Answer(cmd, false, msg); + } + } + try { + vdi.destroy(conn); + } catch (Exception e) { + String msg = "VDI destroy for " + volumeUUID + " failed due to " + e.toString(); + s_logger.warn(msg, e); + return new Answer(cmd, false, msg); + } + + return new Answer(cmd, true, "Success"); + } + + public ShareAnswer execute(final ShareCommand cmd) { + if (!cmd.isShare()) { + SR sr = getISOSRbyVmName(cmd.getVmName()); + Connection conn = getConnection(); + try { + if (sr != null) { + Set vms = VM.getByNameLabel(conn, cmd.getVmName()); + if (vms.size() == 0) { + removeSR(sr); + } + } + } catch (Exception e) { + String msg = "SR.getNameLabel failed due to " + e.getMessage() + e.toString(); + s_logger.warn(msg); + } + } + return new ShareAnswer(cmd, new HashMap()); + } + + public CopyVolumeAnswer execute(final CopyVolumeCommand cmd) { + String volumeUUID = cmd.getVolumePath(); + StoragePoolVO pool = cmd.getPool(); + String secondaryStorageURL = cmd.getSecondaryStorageURL(); + + URI uri = null; + try { + uri = new URI(secondaryStorageURL); + } catch (URISyntaxException e) { + return new CopyVolumeAnswer(cmd, false, "Invalid secondary storage URL specified.", null, null); + } + + String remoteVolumesMountPath = uri.getHost() + ":" + uri.getPath() + "/volumes/"; + String volumeFolder = String.valueOf(cmd.getVolumeId()) + "/"; + boolean toSecondaryStorage = cmd.toSecondaryStorage(); + + String errorMsg = "Failed to copy volume"; + SR primaryStoragePool = null; + SR secondaryStorage = null; + VDI srcVolume = null; + VDI destVolume = null; + Connection conn = getConnection(); + try { + if (toSecondaryStorage) { + // Create the volume folder + if (!createSecondaryStorageFolder(remoteVolumesMountPath, volumeFolder)) { + throw new InternalErrorException("Failed to create the volume folder."); + } + + // Create a SR for the volume UUID folder + secondaryStorage = createNfsSRbyURI(new URI(secondaryStorageURL + "/volumes/" + volumeFolder), false); + + // Look up the volume on the source primary storage pool + srcVolume = getVDIbyUuid(volumeUUID); + + // Copy the volume to secondary storage + destVolume = srcVolume.copy(conn, secondaryStorage); + } else { + // Mount the volume folder + secondaryStorage = createNfsSRbyURI(new URI(secondaryStorageURL + "/volumes/" + volumeFolder), false); + + // Look up the volume on secondary storage + Set vdis = secondaryStorage.getVDIs(conn); + for (VDI vdi : vdis) { + if (vdi.getUuid(conn).equals(volumeUUID)) { + srcVolume = vdi; + break; + } + } + + if (srcVolume == null) { + throw new InternalErrorException("Failed to find volume on secondary storage."); + } + + // Copy the volume to the primary storage pool + primaryStoragePool = getStorageRepository(conn, pool); + destVolume = srcVolume.copy(conn, primaryStoragePool); + } + + String srUUID; + + if (primaryStoragePool == null) { + srUUID = secondaryStorage.getUuid(conn); + } else { + srUUID = primaryStoragePool.getUuid(conn); + } + + String destVolumeUUID = destVolume.getUuid(conn); + + return new CopyVolumeAnswer(cmd, true, null, srUUID, destVolumeUUID); + } catch (XenAPIException e) { + s_logger.warn(errorMsg + ": " + e.toString(), e); + return new CopyVolumeAnswer(cmd, false, e.toString(), null, null); + } catch (Exception e) { + s_logger.warn(errorMsg + ": " + e.toString(), e); + return new CopyVolumeAnswer(cmd, false, e.getMessage(), null, null); + } finally { + if (!toSecondaryStorage && srcVolume != null) { + // Delete the volume on secondary storage + destroyVDI(srcVolume); + } + + removeSR(secondaryStorage); + if (!toSecondaryStorage) { + // Delete the volume folder on secondary storage + deleteSecondaryStorageFolder(remoteVolumesMountPath, volumeFolder); + } + } + + } + + protected AttachVolumeAnswer execute(final AttachVolumeCommand cmd) { + boolean attach = cmd.getAttach(); + String vmName = cmd.getVmName(); + Long deviceId = cmd.getDeviceId(); + + String errorMsg; + if (attach) { + errorMsg = "Failed to attach volume"; + } else { + errorMsg = "Failed to detach volume"; + } + + Connection conn = getConnection(); + try { + // Look up the VDI + VDI vdi = mount(cmd.getPooltype(), cmd.getVolumeFolder(),cmd.getVolumePath()); + // Look up the VM + VM vm = getVM(conn, vmName); + /* For HVM guest, if no pv driver installed, no attach/detach */ + boolean isHVM; + if (vm.getPVBootloader(conn).equalsIgnoreCase("")) + isHVM = true; + else + isHVM = false; + VMGuestMetrics vgm = vm.getGuestMetrics(conn); + boolean pvDrvInstalled = false; + if (!isRefNull(vgm) && vgm.getPVDriversUpToDate(conn)) { + pvDrvInstalled = true; + } + if (isHVM && !pvDrvInstalled) { + s_logger.warn(errorMsg + ": You attempted an operation on a VM which requires PV drivers to be installed but the drivers were not detected"); + return new AttachVolumeAnswer(cmd, "You attempted an operation that requires PV drivers to be installed on the VM. Please install them by inserting xen-pv-drv.iso."); + } + if (attach) { + // Figure out the disk number to attach the VM to + String diskNumber = null; + if( deviceId != null ) { + if( deviceId.longValue() == 3 ) { + String msg = "Device 3 is reserved for CD-ROM, choose other device"; + return new AttachVolumeAnswer(cmd,msg); + } + if(isDeviceUsed(vm, deviceId)) { + String msg = "Device " + deviceId + " is used in VM " + vmName; + return new AttachVolumeAnswer(cmd,msg); + } + diskNumber = deviceId.toString(); + } else { + diskNumber = getUnusedDeviceNum(vm); + } + + + // Create a new VBD + VBD.Record vbdr = new VBD.Record(); + vbdr.VM = vm; + vbdr.VDI = vdi; + vbdr.bootable = false; + vbdr.userdevice = diskNumber; + vbdr.mode = Types.VbdMode.RW; + vbdr.type = Types.VbdType.DISK; + vbdr.unpluggable = true; + VBD vbd = VBD.create(conn, vbdr); + + // Attach the VBD to the VM + vbd.plug(conn); + + // Update the VDI's label to include the VM name + vdi.setNameLabel(conn, vmName + "-DATA"); + + return new AttachVolumeAnswer(cmd, Long.parseLong(diskNumber)); + } else { + // Look up all VBDs for this VDI + Set vbds = vdi.getVBDs(conn); + + // Detach each VBD from its VM, and then destroy it + for (VBD vbd : vbds) { + VBD.Record vbdr = vbd.getRecord(conn); + + if (vbdr.currentlyAttached) { + vbd.unplug(conn); + } + + vbd.destroy(conn); + } + + // Update the VDI's label to be "detached" + vdi.setNameLabel(conn, "detached"); + + umount(vdi); + + return new AttachVolumeAnswer(cmd); + } + } catch (XenAPIException e) { + String msg = errorMsg + " for uuid: " + cmd.getVolumePath() + " due to " + e.toString(); + s_logger.warn(msg, e); + return new AttachVolumeAnswer(cmd, msg); + } catch (Exception e) { + String msg = errorMsg + " for uuid: " + cmd.getVolumePath() + " due to " + e.getMessage(); + s_logger.warn(msg, e); + return new AttachVolumeAnswer(cmd, msg); + } + + } + + protected void umount(VDI vdi) { + + } + + protected Answer execute(final AttachIsoCommand cmd) { + boolean attach = cmd.isAttach(); + String vmName = cmd.getVmName(); + String isoURL = cmd.getIsoPath(); + + String errorMsg; + if (attach) { + errorMsg = "Failed to attach ISO"; + } else { + errorMsg = "Failed to detach ISO"; + } + + Connection conn = getConnection(); + try { + if (attach) { + VBD isoVBD = null; + + // Find the VM + VM vm = getVM(conn, vmName); + + // Find the ISO VDI + VDI isoVDI = getIsoVDIByURL(conn, vmName, isoURL); + + // Find the VM's CD-ROM VBD + Set vbds = vm.getVBDs(conn); + for (VBD vbd : vbds) { + String userDevice = vbd.getUserdevice(conn); + Types.VbdType type = vbd.getType(conn); + + if (userDevice.equals("3") && type == Types.VbdType.CD) { + isoVBD = vbd; + break; + } + } + + if (isoVBD == null) { + throw new CloudRuntimeException("Unable to find CD-ROM VBD for VM: " + vmName); + } else { + // If an ISO is already inserted, eject it + if (isoVBD.getEmpty(conn) == false) { + isoVBD.eject(conn); + } + + // Insert the new ISO + isoVBD.insert(conn, isoVDI); + } + + return new Answer(cmd); + } else { + // Find the VM + VM vm = getVM(conn, vmName); + String vmUUID = vm.getUuid(conn); + + // Find the ISO VDI + VDI isoVDI = getIsoVDIByURL(conn, vmName, isoURL); + + SR sr = isoVDI.getSR(conn); + + // Look up all VBDs for this VDI + Set vbds = isoVDI.getVBDs(conn); + + // Iterate through VBDs, and if the VBD belongs the VM, eject + // the ISO from it + for (VBD vbd : vbds) { + VM vbdVM = vbd.getVM(conn); + String vbdVmUUID = vbdVM.getUuid(conn); + + if (vbdVmUUID.equals(vmUUID)) { + // If an ISO is already inserted, eject it + if (!vbd.getEmpty(conn)) { + vbd.eject(conn); + } + + break; + } + } + + if (!sr.getNameLabel(conn).startsWith("XenServer Tools")) { + removeSR(sr); + } + + return new Answer(cmd); + } + } catch (XenAPIException e) { + s_logger.warn(errorMsg + ": " + e.toString(), e); + return new Answer(cmd, false, e.toString()); + } catch (Exception e) { + s_logger.warn(errorMsg + ": " + e.toString(), e); + return new Answer(cmd, false, e.getMessage()); + } + } + + protected ValidateSnapshotAnswer execute(final ValidateSnapshotCommand cmd) { + String primaryStoragePoolNameLabel = cmd.getPrimaryStoragePoolNameLabel(); + String volumeUuid = cmd.getVolumeUuid(); // Precondition: not null + String firstBackupUuid = cmd.getFirstBackupUuid(); + String previousSnapshotUuid = cmd.getPreviousSnapshotUuid(); + String templateUuid = cmd.getTemplateUuid(); + + // By default assume failure + String details = "Could not validate previous snapshot backup UUID " + "because the primary Storage SR could not be created from the name label: " + + primaryStoragePoolNameLabel; + boolean success = false; + String expectedSnapshotBackupUuid = null; + String actualSnapshotBackupUuid = null; + String actualSnapshotUuid = null; + + Boolean isISCSI = false; + String primaryStorageSRUuid = null; + Connection conn = getConnection(); + try { + SR primaryStorageSR = getSRByNameLabelandHost(primaryStoragePoolNameLabel); + + if (primaryStorageSR != null) { + primaryStorageSRUuid = primaryStorageSR.getUuid(conn); + isISCSI = SRType.LVMOISCSI.equals(primaryStorageSR.getType(conn)); + } + } catch (BadServerResponse e) { + details += ", reason: " + e.getMessage(); + s_logger.error(details, e); + } catch (XenAPIException e) { + details += ", reason: " + e.getMessage(); + s_logger.error(details, e); + } catch (XmlRpcException e) { + details += ", reason: " + e.getMessage(); + s_logger.error(details, e); + } + + if (primaryStorageSRUuid != null) { + if (templateUuid == null) { + templateUuid = ""; + } + if (firstBackupUuid == null) { + firstBackupUuid = ""; + } + if (previousSnapshotUuid == null) { + previousSnapshotUuid = ""; + } + String result = callHostPlugin("vmopsSnapshot", "validateSnapshot", "primaryStorageSRUuid", primaryStorageSRUuid, "volumeUuid", volumeUuid, "firstBackupUuid", firstBackupUuid, + "previousSnapshotUuid", previousSnapshotUuid, "templateUuid", templateUuid, "isISCSI", isISCSI.toString()); + if (result == null || result.isEmpty()) { + details = "Validating snapshot backup for volume with UUID: " + volumeUuid + " failed because there was an exception in the plugin"; + // callHostPlugin exception which has been logged already + } else { + String[] uuids = result.split("#", -1); + if (uuids.length >= 3) { + expectedSnapshotBackupUuid = uuids[1]; + actualSnapshotBackupUuid = uuids[2]; + } + if (uuids.length >= 4) { + actualSnapshotUuid = uuids[3]; + } else { + actualSnapshotUuid = ""; + } + if (uuids[0].equals("1")) { + success = true; + details = null; + } else { + details = "Previous snapshot backup on the primary storage is invalid. " + "Expected: " + expectedSnapshotBackupUuid + " Actual: " + actualSnapshotBackupUuid; + // success is still false + } + s_logger.debug("ValidatePreviousSnapshotBackup returned " + " success: " + success + " details: " + details + " expectedSnapshotBackupUuid: " + + expectedSnapshotBackupUuid + " actualSnapshotBackupUuid: " + actualSnapshotBackupUuid + " actualSnapshotUuid: " + actualSnapshotUuid); + } + } + + return new ValidateSnapshotAnswer(cmd, success, details, expectedSnapshotBackupUuid, actualSnapshotBackupUuid, actualSnapshotUuid); + } + + protected ManageSnapshotAnswer execute(final ManageSnapshotCommand cmd) { + long snapshotId = cmd.getSnapshotId(); + String snapshotName = cmd.getSnapshotName(); + + // By default assume failure + boolean success = false; + String cmdSwitch = cmd.getCommandSwitch(); + String snapshotOp = "Unsupported snapshot command." + cmdSwitch; + if (cmdSwitch.equals(ManageSnapshotCommand.CREATE_SNAPSHOT)) { + snapshotOp = "create"; + } else if (cmdSwitch.equals(ManageSnapshotCommand.DESTROY_SNAPSHOT)) { + snapshotOp = "destroy"; + } + String details = "ManageSnapshotCommand operation: " + snapshotOp + " Failed for snapshotId: " + snapshotId; + String snapshotUUID = null; + + Connection conn = getConnection(); + try { + if (cmdSwitch.equals(ManageSnapshotCommand.CREATE_SNAPSHOT)) { + // Look up the volume + String volumeUUID = cmd.getVolumePath(); + + VDI volume = getVDIbyUuid(volumeUUID); + + // Create a snapshot + VDI snapshot = volume.snapshot(conn, new HashMap()); + + if (snapshotName != null) { + snapshot.setNameLabel(conn, snapshotName); + } + + // Determine the UUID of the snapshot + VDI.Record vdir = snapshot.getRecord(conn); + snapshotUUID = vdir.uuid; + + success = true; + details = null; + } else if (cmd.getCommandSwitch().equals(ManageSnapshotCommand.DESTROY_SNAPSHOT)) { + // Look up the snapshot + snapshotUUID = cmd.getSnapshotPath(); + VDI snapshot = getVDIbyUuid(snapshotUUID); + + snapshot.destroy(conn); + snapshotUUID = null; + success = true; + details = null; + } + } catch (XenAPIException e) { + details += ", reason: " + e.toString(); + s_logger.warn(details, e); + } catch (Exception e) { + details += ", reason: " + e.toString(); + s_logger.warn(details, e); + } + + return new ManageSnapshotAnswer(cmd, snapshotId, snapshotUUID, success, details); + } + + protected CreatePrivateTemplateAnswer execute(final CreatePrivateTemplateCommand cmd) { + String secondaryStorageURL = cmd.getSecondaryStorageURL(); + String snapshotUUID = cmd.getSnapshotPath(); + String userSpecifiedName = cmd.getTemplateName(); + + SR secondaryStorage = null; + VDI privateTemplate = null; + Connection conn = getConnection(); + try { + URI uri = new URI(secondaryStorageURL); + String remoteTemplateMountPath = uri.getHost() + ":" + uri.getPath() + "/template/"; + String templateFolder = cmd.getAccountId() + "/" + cmd.getTemplateId() + "/"; + String templateDownloadFolder = createTemplateDownloadFolder(remoteTemplateMountPath, templateFolder); + String templateInstallFolder = "tmpl/" + templateFolder; + + // Create a SR for the secondary storage download folder + secondaryStorage = createNfsSRbyURI(new URI(secondaryStorageURL + "/template/" + templateDownloadFolder), false); + + // Look up the snapshot and copy it to secondary storage + VDI snapshot = getVDIbyUuid(snapshotUUID); + privateTemplate = snapshot.copy(conn, secondaryStorage); + + if (userSpecifiedName != null) { + privateTemplate.setNameLabel(conn, userSpecifiedName); + } + + // Determine the template file name and install path + VDI.Record vdir = privateTemplate.getRecord(conn); + String templateName = vdir.uuid; + String templateFilename = templateName + ".vhd"; + String installPath = "template/" + templateInstallFolder + templateFilename; + + // Determine the template's virtual size and then forget the VDI + long virtualSize = privateTemplate.getVirtualSize(conn); + // Create the template.properties file in the download folder, move + // the template and the template.properties file + // to the install folder, and then delete the download folder + if (!postCreatePrivateTemplate(remoteTemplateMountPath, templateDownloadFolder, templateInstallFolder, templateFilename, templateName, userSpecifiedName, null, + virtualSize, cmd.getTemplateId())) { + throw new InternalErrorException("Failed to create the template.properties file."); + } + + return new CreatePrivateTemplateAnswer(cmd, true, null, installPath, virtualSize, templateName, ImageFormat.VHD); + } catch (XenAPIException e) { + if (privateTemplate != null) { + destroyVDI(privateTemplate); + } + + s_logger.warn("CreatePrivateTemplate Failed due to " + e.toString(), e); + return new CreatePrivateTemplateAnswer(cmd, false, e.toString(), null, 0, null, null); + } catch (Exception e) { + s_logger.warn("CreatePrivateTemplate Failed due to " + e.getMessage(), e); + return new CreatePrivateTemplateAnswer(cmd, false, e.getMessage(), null, 0, null, null); + } finally { + // Remove the secondary storage SR + removeSR(secondaryStorage); + } + } + + private String createTemplateDownloadFolder(String remoteTemplateMountPath, String templateFolder) throws InternalErrorException, URISyntaxException { + String templateDownloadFolder = "download/" + _host.uuid + "/" + templateFolder; + + // Create the download folder + if (!createSecondaryStorageFolder(remoteTemplateMountPath, templateDownloadFolder)) { + throw new InternalErrorException("Failed to create the template download folder."); + } + return templateDownloadFolder; + } + + protected CreatePrivateTemplateAnswer execute(final CreatePrivateTemplateFromSnapshotCommand cmd) { + String primaryStorageNameLabel = cmd.getPrimaryStoragePoolNameLabel(); + Long dcId = cmd.getDataCenterId(); + Long accountId = cmd.getAccountId(); + Long volumeId = cmd.getVolumeId(); + String secondaryStoragePoolURL = cmd.getSecondaryStoragePoolURL(); + String backedUpSnapshotUuid = cmd.getSnapshotUuid(); + String origTemplateInstallPath = cmd.getOrigTemplateInstallPath(); + Long newTemplateId = cmd.getNewTemplateId(); + String userSpecifiedName = cmd.getTemplateName(); + + // By default, assume failure + String details = "Failed to create private template " + newTemplateId + " from snapshot for volume: " + volumeId + " with backupUuid: " + backedUpSnapshotUuid; + String newTemplatePath = null; + String templateName = null; + boolean result = false; + long virtualSize = 0; + try { + URI uri = new URI(secondaryStoragePoolURL); + String remoteTemplateMountPath = uri.getHost() + ":" + uri.getPath() + "/template/"; + String templateFolder = cmd.getAccountId() + "/" + newTemplateId + "/"; + String templateDownloadFolder = createTemplateDownloadFolder(remoteTemplateMountPath, templateFolder); + String templateInstallFolder = "tmpl/" + templateFolder; + // Yes, create a template vhd + Pair vhdDetails = createVHDFromSnapshot(primaryStorageNameLabel, dcId, accountId, volumeId, secondaryStoragePoolURL, backedUpSnapshotUuid, + origTemplateInstallPath, templateDownloadFolder); + + VHDInfo vhdInfo = vhdDetails.first(); + String failureDetails = vhdDetails.second(); + if (vhdInfo == null) { + if (failureDetails != null) { + details += failureDetails; + } + } else { + templateName = vhdInfo.getUuid(); + String templateFilename = templateName + ".vhd"; + String templateInstallPath = templateInstallFolder + "/" + templateFilename; + + newTemplatePath = "template" + "/" + templateInstallPath; + + virtualSize = vhdInfo.getVirtualSize(); + // create the template.properties file + result = postCreatePrivateTemplate(remoteTemplateMountPath, templateDownloadFolder, templateInstallFolder, templateFilename, templateName, userSpecifiedName, null, + virtualSize, newTemplateId); + if (!result) { + details += ", reason: Could not create the template.properties file on secondary storage dir: " + templateInstallFolder; + } else { + // Aaah, success. + details = null; + } + + } + } catch (XenAPIException e) { + details += ", reason: " + e.getMessage(); + s_logger.error(details, e); + } catch (Exception e) { + details += ", reason: " + e.getMessage(); + s_logger.error(details, e); + } + return new CreatePrivateTemplateAnswer(cmd, result, details, newTemplatePath, virtualSize, templateName, ImageFormat.VHD); + } + + protected BackupSnapshotAnswer execute(final BackupSnapshotCommand cmd) { + String primaryStorageNameLabel = cmd.getPrimaryStoragePoolNameLabel(); + Long dcId = cmd.getDataCenterId(); + Long accountId = cmd.getAccountId(); + Long volumeId = cmd.getVolumeId(); + String secondaryStoragePoolURL = cmd.getSecondaryStoragePoolURL(); + String snapshotUuid = cmd.getSnapshotUuid(); // not null: Precondition. + String prevSnapshotUuid = cmd.getPrevSnapshotUuid(); + String prevBackupUuid = cmd.getPrevBackupUuid(); + boolean isFirstSnapshotOfRootVolume = cmd.isFirstSnapshotOfRootVolume(); + // By default assume failure + String details = null; + boolean success = false; + String snapshotBackupUuid = null; + try { + Connection conn = getConnection(); + SR primaryStorageSR = getSRByNameLabelandHost(primaryStorageNameLabel); + if (primaryStorageSR == null) { + throw new InternalErrorException("Could not backup snapshot because the primary Storage SR could not be created from the name label: " + primaryStorageNameLabel); + } + String primaryStorageSRUuid = primaryStorageSR.getUuid(conn); + Boolean isISCSI = SRType.LVMOISCSI.equals(primaryStorageSR.getType(conn)); + + URI uri = new URI(secondaryStoragePoolURL); + String secondaryStorageMountPath = uri.getHost() + ":" + uri.getPath(); + + if (secondaryStorageMountPath == null) { + details = "Couldn't backup snapshot because the URL passed: " + secondaryStoragePoolURL + " is invalid."; + } else { + boolean gcHappened = true; + if (gcHappened) { + snapshotBackupUuid = backupSnapshot(primaryStorageSRUuid, dcId, accountId, volumeId, secondaryStorageMountPath, snapshotUuid, prevSnapshotUuid, prevBackupUuid, + isFirstSnapshotOfRootVolume, isISCSI); + success = (snapshotBackupUuid != null); + } else { + s_logger.warn("GC hasn't happened yet for previousSnapshotUuid: " + prevSnapshotUuid + ". Will retry again after 1 min"); + } + } + + if (!success) { + // Mark the snapshot as removed in the database. + // When the next snapshot is taken, it will be + // 1) deleted from the DB 2) The snapshotUuid will be deleted from the primary + // 3) the snapshotBackupUuid will be copied to secondary + // 4) if possible it will be coalesced with the next snapshot. + + } else if (prevSnapshotUuid != null && !isFirstSnapshotOfRootVolume) { + // Destroy the previous snapshot, if it exists. + // We destroy the previous snapshot only if the current snapshot + // backup succeeds. + // The aim is to keep the VDI of the last 'successful' snapshot + // so that it doesn't get merged with the + // new one + // and muddle the vhd chain on the secondary storage. + details = "Successfully backedUp the snapshotUuid: " + snapshotUuid + " to secondary storage."; + destroySnapshotOnPrimaryStorage(prevSnapshotUuid); + } + + } catch (XenAPIException e) { + details = "BackupSnapshot Failed due to " + e.toString(); + s_logger.warn(details, e); + } catch (Exception e) { + details = "BackupSnapshot Failed due to " + e.getMessage(); + s_logger.warn(details, e); + } + + return new BackupSnapshotAnswer(cmd, success, details, snapshotBackupUuid); + } + + protected CreateVolumeFromSnapshotAnswer execute(final CreateVolumeFromSnapshotCommand cmd) { + String primaryStorageNameLabel = cmd.getPrimaryStoragePoolNameLabel(); + Long dcId = cmd.getDataCenterId(); + Long accountId = cmd.getAccountId(); + Long volumeId = cmd.getVolumeId(); + String secondaryStoragePoolURL = cmd.getSecondaryStoragePoolURL(); + String backedUpSnapshotUuid = cmd.getSnapshotUuid(); + String templatePath = cmd.getTemplatePath(); + + // By default, assume the command has failed and set the params to be + // passed to CreateVolumeFromSnapshotAnswer appropriately + boolean result = false; + // Generic error message. + String details = "Failed to create volume from snapshot for volume: " + volumeId + " with backupUuid: " + backedUpSnapshotUuid; + String vhdUUID = null; + SR temporarySROnSecondaryStorage = null; + String mountPointOfTemporaryDirOnSecondaryStorage = null; + try { + VDI vdi = null; + Connection conn = getConnection(); + SR primaryStorageSR = getSRByNameLabelandHost(primaryStorageNameLabel); + if (primaryStorageSR == null) { + throw new InternalErrorException("Could not create volume from snapshot because the primary Storage SR could not be created from the name label: " + + primaryStorageNameLabel); + } + + Boolean isISCSI = SRType.LVMOISCSI.equals(primaryStorageSR.getType(conn)); + + // Get the absolute path of the template on the secondary storage. + URI uri = new URI(secondaryStoragePoolURL); + String secondaryStorageMountPath = uri.getHost() + ":" + uri.getPath(); + + if (secondaryStorageMountPath == null) { + details += " because the URL passed: " + secondaryStoragePoolURL + " is invalid."; + return new CreateVolumeFromSnapshotAnswer(cmd, result, details, vhdUUID); + } + + // Create a volume and not a template + String templateDownloadFolder = ""; + + VHDInfo vhdInfo = createVHDFromSnapshot(dcId, accountId, volumeId, secondaryStorageMountPath, backedUpSnapshotUuid, templatePath, templateDownloadFolder, isISCSI); + if (vhdInfo == null) { + details += " because the vmops plugin on XenServer failed at some point"; + } else { + vhdUUID = vhdInfo.getUuid(); + String tempDirRelativePath = "snapshots" + File.separator + accountId + File.separator + volumeId + "_temp"; + mountPointOfTemporaryDirOnSecondaryStorage = secondaryStorageMountPath + File.separator + tempDirRelativePath; + + uri = new URI("nfs://" + mountPointOfTemporaryDirOnSecondaryStorage); + // No need to check if the SR already exists. It's a temporary + // SR destroyed when this method exits. + // And two createVolumeFromSnapshot operations cannot proceed at + // the same time. + temporarySROnSecondaryStorage = createNfsSRbyURI(uri, false); + if (temporarySROnSecondaryStorage == null) { + details += "because SR couldn't be created on " + mountPointOfTemporaryDirOnSecondaryStorage; + } else { + s_logger.debug("Successfully created temporary SR on secondary storage " + temporarySROnSecondaryStorage.getNameLabel(conn) + "with uuid " + + temporarySROnSecondaryStorage.getUuid(conn) + " and scanned it"); + // createNFSSRbyURI also scans the SR and introduces the VDI + + vdi = getVDIbyUuid(vhdUUID); + + if (vdi != null) { + s_logger.debug("Successfully created VDI on secondary storage SR " + temporarySROnSecondaryStorage.getNameLabel(conn) + " with uuid " + vhdUUID); + s_logger.debug("Copying VDI: " + vdi.getLocation(conn) + " from secondary to primary"); + VDI vdiOnPrimaryStorage = vdi.copy(conn, primaryStorageSR); + // vdi.copy introduces the vdi into the database. Don't + // need to do a scan on the primary + // storage. + + if (vdiOnPrimaryStorage != null) { + vhdUUID = vdiOnPrimaryStorage.getUuid(conn); + s_logger.debug("Successfully copied and introduced VDI on primary storage with path " + vdiOnPrimaryStorage.getLocation(conn) + " and uuid " + vhdUUID); + result = true; + details = null; + + } else { + details += ". Could not copy the vdi " + vhdUUID + " to primary storage"; + } + + // The VHD on temporary was scanned and introduced as a VDI + // destroy it as we don't need it anymore. + vdi.destroy(conn); + } else { + details += ". Could not scan and introduce vdi with uuid: " + vhdUUID; + } + } + } + } catch (XenAPIException e) { + details += " due to " + e.toString(); + s_logger.warn(details, e); + } catch (Exception e) { + details += " due to " + e.getMessage(); + s_logger.warn(details, e); + } finally { + // In all cases, if the temporary SR was created, forget it. + if (temporarySROnSecondaryStorage != null) { + removeSR(temporarySROnSecondaryStorage); + // Delete the temporary directory created. + File folderPath = new File(mountPointOfTemporaryDirOnSecondaryStorage); + String remoteMountPath = folderPath.getParent(); + String folder = folderPath.getName(); + deleteSecondaryStorageFolder(remoteMountPath, folder); + } + } + if (!result) { + // Is this logged at a higher level? + s_logger.error(details); + } + + // In all cases return something. + return new CreateVolumeFromSnapshotAnswer(cmd, result, details, vhdUUID); + } + + protected DeleteSnapshotBackupAnswer execute(final DeleteSnapshotBackupCommand cmd) { + Long dcId = cmd.getDataCenterId(); + Long accountId = cmd.getAccountId(); + Long volumeId = cmd.getVolumeId(); + String secondaryStoragePoolURL = cmd.getSecondaryStoragePoolURL(); + String backupUUID = cmd.getSnapshotUuid(); + String childUUID = cmd.getChildUUID(); + String primaryStorageNameLabel = cmd.getPrimaryStoragePoolNameLabel(); + + String details = null; + boolean success = false; + + SR primaryStorageSR = null; + Boolean isISCSI = false; + try { + Connection conn = getConnection(); + primaryStorageSR = getSRByNameLabelandHost(primaryStorageNameLabel); + if (primaryStorageSR == null) { + details = "Primary Storage SR could not be created from the name label: " + primaryStorageNameLabel; + throw new InternalErrorException(details); + } + isISCSI = SRType.LVMOISCSI.equals(primaryStorageSR.getType(conn)); + } catch (XenAPIException e) { + details = "Couldn't determine primary SR type " + e.getMessage(); + s_logger.error(details, e); + } catch (Exception e) { + details = "Couldn't determine primary SR type " + e.getMessage(); + s_logger.error(details, e); + } + + if (primaryStorageSR != null) { + URI uri = null; + try { + uri = new URI(secondaryStoragePoolURL); + } catch (URISyntaxException e) { + details = "Error finding the secondary storage URL" + e.getMessage(); + s_logger.error(details, e); + } + if (uri != null) { + String secondaryStorageMountPath = uri.getHost() + ":" + uri.getPath(); + + if (secondaryStorageMountPath == null) { + details = "Couldn't delete snapshot because the URL passed: " + secondaryStoragePoolURL + " is invalid."; + } else { + details = deleteSnapshotBackup(dcId, accountId, volumeId, secondaryStorageMountPath, backupUUID, childUUID, isISCSI); + success = (details != null && details.equals("1")); + if (success) { + s_logger.debug("Successfully deleted snapshot backup " + backupUUID); + } + } + } + } + return new DeleteSnapshotBackupAnswer(cmd, success, details); + } + + protected Answer execute(DeleteSnapshotsDirCommand cmd) { + Long dcId = cmd.getDataCenterId(); + Long accountId = cmd.getAccountId(); + Long volumeId = cmd.getVolumeId(); + String secondaryStoragePoolURL = cmd.getSecondaryStoragePoolURL(); + String snapshotUUID = cmd.getSnapshotUuid(); + String primaryStorageNameLabel = cmd.getPrimaryStoragePoolNameLabel(); + + String details = null; + boolean success = false; + + SR primaryStorageSR = null; + try { + primaryStorageSR = getSRByNameLabelandHost(primaryStorageNameLabel); + if (primaryStorageSR == null) { + details = "Primary Storage SR could not be created from the name label: " + primaryStorageNameLabel; + } + } catch (XenAPIException e) { + details = "Couldn't determine primary SR type " + e.getMessage(); + s_logger.error(details, e); + } catch (Exception e) { + details = "Couldn't determine primary SR type " + e.getMessage(); + s_logger.error(details, e); + } + + if (primaryStorageSR != null) { + if (snapshotUUID != null) { + VDI snapshotVDI = getVDIbyUuid(snapshotUUID); + if (snapshotVDI != null) { + destroyVDI(snapshotVDI); + } + } + } + URI uri = null; + try { + uri = new URI(secondaryStoragePoolURL); + } catch (URISyntaxException e) { + details = "Error finding the secondary storage URL" + e.getMessage(); + s_logger.error(details, e); + } + if (uri != null) { + String secondaryStorageMountPath = uri.getHost() + ":" + uri.getPath(); + + if (secondaryStorageMountPath == null) { + details = "Couldn't delete snapshotsDir because the URL passed: " + secondaryStoragePoolURL + " is invalid."; + } else { + details = deleteSnapshotsDir(dcId, accountId, volumeId, secondaryStorageMountPath); + success = (details != null && details.equals("1")); + if (success) { + s_logger.debug("Successfully deleted snapshotsDir for volume: " + volumeId); + } + } + } + + return new Answer(cmd, success, details); + } + + protected VM getVM(Connection conn, String vmName) { + // Look up VMs with the specified name + Set vms; + try { + vms = VM.getByNameLabel(conn, vmName); + } catch (XenAPIException e) { + throw new CloudRuntimeException("Unable to get " + vmName + ": " + e.toString(), e); + } catch (Exception e) { + throw new CloudRuntimeException("Unable to get " + vmName + ": " + e.getMessage(), e); + } + + // If there are no VMs, throw an exception + if (vms.size() == 0) + throw new CloudRuntimeException("VM with name: " + vmName + " does not exist."); + + // If there is more than one VM, print a warning + if (vms.size() > 1) + s_logger.warn("Found " + vms.size() + " VMs with name: " + vmName); + + // Return the first VM in the set + return vms.iterator().next(); + } + + protected VDI getIsoVDIByURL(Connection conn, String vmName, String isoURL) { + SR isoSR = null; + String mountpoint = null; + if (isoURL.startsWith("xs-tools")) { + try { + Set vdis = VDI.getByNameLabel(conn, isoURL); + if (vdis.isEmpty()) { + throw new CloudRuntimeException("Could not find ISO with URL: " + isoURL); + } + return vdis.iterator().next(); + + } catch (XenAPIException e) { + throw new CloudRuntimeException("Unable to get pv iso: " + isoURL + " due to " + e.toString()); + } catch (Exception e) { + throw new CloudRuntimeException("Unable to get pv iso: " + isoURL + " due to " + e.toString()); + } + } + + int index = isoURL.lastIndexOf("/"); + mountpoint = isoURL.substring(0, index); + + URI uri; + try { + uri = new URI(mountpoint); + } catch (URISyntaxException e) { + // TODO Auto-generated catch block + throw new CloudRuntimeException("isoURL is wrong: " + isoURL); + } + isoSR = getISOSRbyVmName(vmName); + if (isoSR == null) { + isoSR = createIsoSRbyURI(uri, vmName, false); + } + + String isoName = isoURL.substring(index + 1); + + VDI isoVDI = getVDIbyLocationandSR(isoName, isoSR); + + if (isoVDI != null) { + return isoVDI; + } else { + throw new CloudRuntimeException("Could not find ISO with URL: " + isoURL); + } + } + + protected SR getStorageRepository(Connection conn, StoragePoolTO pool) { + Set srs; + try { + srs = SR.getByNameLabel(conn, pool.getUuid()); + } catch (XenAPIException e) { + throw new CloudRuntimeException("Unable to get SR " + pool.getUuid() + " due to " + e.toString(), e); + } catch (Exception e) { + throw new CloudRuntimeException("Unable to get SR " + pool.getUuid() + " due to " + e.getMessage(), e); + } + + if (srs.size() > 1) { + throw new CloudRuntimeException("More than one storage repository was found for pool with uuid: " + pool.getUuid()); + } + + if (srs.size() == 1) { + SR sr = srs.iterator().next(); + if (s_logger.isDebugEnabled()) { + s_logger.debug("SR retrieved for " + pool.getId() + " is mapped to " + sr.toString()); + } + + if (checkSR(sr)) { + return sr; + } + } + + if (pool.getType() == StoragePoolType.NetworkFilesystem) + return getNfsSR(conn, pool); + else if (pool.getType() == StoragePoolType.IscsiLUN) + return getIscsiSR(conn, pool); + else + throw new CloudRuntimeException("The pool type: " + pool.getType().name() + " is not supported."); + + } + + protected SR getStorageRepository(Connection conn, StoragePoolVO pool) { + Set srs; + try { + srs = SR.getByNameLabel(conn, pool.getUuid()); + } catch (XenAPIException e) { + throw new CloudRuntimeException("Unable to get SR " + pool.getUuid() + " due to " + e.toString(), e); + } catch (Exception e) { + throw new CloudRuntimeException("Unable to get SR " + pool.getUuid() + " due to " + e.getMessage(), e); + } + + if (srs.size() > 1) { + throw new CloudRuntimeException("More than one storage repository was found for pool with uuid: " + pool.getUuid()); + } else if (srs.size() == 1) { + SR sr = srs.iterator().next(); + if (s_logger.isDebugEnabled()) { + s_logger.debug("SR retrieved for " + pool.getId() + " is mapped to " + sr.toString()); + } + + if (checkSR(sr)) { + return sr; + } + } + + if (pool.getPoolType() == StoragePoolType.NetworkFilesystem) + return getNfsSR(pool); + else if (pool.getPoolType() == StoragePoolType.IscsiLUN) + return getIscsiSR(conn, pool); + else + throw new CloudRuntimeException("The pool type: " + pool.getPoolType().name() + " is not supported."); + + } + + protected Answer execute(final CheckConsoleProxyLoadCommand cmd) { + return executeProxyLoadScan(cmd, cmd.getProxyVmId(), cmd.getProxyVmName(), cmd.getProxyManagementIp(), cmd.getProxyCmdPort()); + } + + protected Answer execute(final WatchConsoleProxyLoadCommand cmd) { + return executeProxyLoadScan(cmd, cmd.getProxyVmId(), cmd.getProxyVmName(), cmd.getProxyManagementIp(), cmd.getProxyCmdPort()); + } + + protected Answer executeProxyLoadScan(final Command cmd, final long proxyVmId, final String proxyVmName, final String proxyManagementIp, final int cmdPort) { + String result = null; + + final StringBuffer sb = new StringBuffer(); + sb.append("http://").append(proxyManagementIp).append(":" + cmdPort).append("/cmd/getstatus"); + + boolean success = true; + try { + final URL url = new URL(sb.toString()); + final URLConnection conn = url.openConnection(); + + // setting TIMEOUTs to avoid possible waiting until death situations + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + final InputStream is = conn.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + final StringBuilder sb2 = new StringBuilder(); + String line = null; + try { + while ((line = reader.readLine()) != null) + sb2.append(line + "\n"); + result = sb2.toString(); + } catch (final IOException e) { + success = false; + } finally { + try { + is.close(); + } catch (final IOException e) { + s_logger.warn("Exception when closing , console proxy address : " + proxyManagementIp); + success = false; + } + } + } catch (final IOException e) { + s_logger.warn("Unable to open console proxy command port url, console proxy address : " + proxyManagementIp); + success = false; + } + + return new ConsoleProxyLoadAnswer(cmd, proxyVmId, proxyVmName, success, result); + } + + protected boolean createSecondaryStorageFolder(String remoteMountPath, String newFolder) { + String result = callHostPlugin("vmopsSnapshot", "create_secondary_storage_folder", "remoteMountPath", remoteMountPath, "newFolder", newFolder); + return (result != null); + } + + protected boolean deleteSecondaryStorageFolder(String remoteMountPath, String folder) { + String result = callHostPlugin("vmopsSnapshot", "delete_secondary_storage_folder", "remoteMountPath", remoteMountPath, "folder", folder); + return (result != null); + } + + protected boolean postCreatePrivateTemplate(String remoteTemplateMountPath, String templateDownloadFolder, String templateInstallFolder, String templateFilename, + String templateName, String templateDescription, String checksum, long virtualSize, long templateId) { + + if (templateDescription == null) { + templateDescription = ""; + } + + if (checksum == null) { + checksum = ""; + } + + String result = callHostPlugin("vmopsSnapshot", "post_create_private_template", "remoteTemplateMountPath", remoteTemplateMountPath, "templateDownloadFolder", templateDownloadFolder, + "templateInstallFolder", templateInstallFolder, "templateFilename", templateFilename, "templateName", templateName, "templateDescription", templateDescription, + "checksum", checksum, "virtualSize", String.valueOf(virtualSize), "templateId", String.valueOf(templateId)); + + boolean success = false; + if (result != null && !result.isEmpty()) { + // Else, command threw an exception which has already been logged. + + String[] tmp = result.split("#"); + String status = tmp[0]; + + if (status != null && status.equalsIgnoreCase("1")) { + s_logger.debug("Successfully created template.properties file on secondary storage dir: " + templateInstallFolder); + success = true; + } else { + s_logger.warn("Could not create template.properties file on secondary storage dir: " + templateInstallFolder + " for templateId: " + templateId + + ". Failed with status " + status); + } + } + + return success; + } + + // Each argument is put in a separate line for readability. + // Using more lines does not harm the environment. + protected String backupSnapshot(String primaryStorageSRUuid, Long dcId, Long accountId, Long volumeId, String secondaryStorageMountPath, String snapshotUuid, + String prevSnapshotUuid, String prevBackupUuid, Boolean isFirstSnapshotOfRootVolume, Boolean isISCSI) { + String backupSnapshotUuid = null; + + if (prevSnapshotUuid == null) { + prevSnapshotUuid = ""; + } + if (prevBackupUuid == null) { + prevBackupUuid = ""; + } + + // Each argument is put in a separate line for readability. + // Using more lines does not harm the environment. + String results = callHostPlugin("vmopsSnapshot", "backupSnapshot", "primaryStorageSRUuid", primaryStorageSRUuid, "dcId", dcId.toString(), "accountId", accountId.toString(), "volumeId", + volumeId.toString(), "secondaryStorageMountPath", secondaryStorageMountPath, "snapshotUuid", snapshotUuid, "prevSnapshotUuid", prevSnapshotUuid, "prevBackupUuid", + prevBackupUuid, "isFirstSnapshotOfRootVolume", isFirstSnapshotOfRootVolume.toString(), "isISCSI", isISCSI.toString()); + + if (results == null || results.isEmpty()) { + // errString is already logged. + return null; + } + + String[] tmp = results.split("#"); + String status = tmp[0]; + backupSnapshotUuid = tmp[1]; + + // status == "1" if and only if backupSnapshotUuid != null + // So we don't rely on status value but return backupSnapshotUuid as an + // indicator of success. + String failureString = "Could not copy backupUuid: " + backupSnapshotUuid + " of volumeId: " + volumeId + " from primary storage " + primaryStorageSRUuid + + " to secondary storage " + secondaryStorageMountPath; + if (status != null && status.equalsIgnoreCase("1") && backupSnapshotUuid != null) { + s_logger.debug("Successfully copied backupUuid: " + backupSnapshotUuid + " of volumeId: " + volumeId + " to secondary storage"); + } else { + s_logger.debug(failureString + ". Failed with status: " + status); + } + + return backupSnapshotUuid; + } + + protected boolean destroySnapshotOnPrimaryStorage(String snapshotUuid) { + // Precondition snapshotUuid != null + try { + Connection conn = getConnection(); + VDI snapshot = getVDIbyUuid(snapshotUuid); + if (snapshot == null) { + throw new InternalErrorException("Could not destroy snapshot " + snapshotUuid + " because the snapshot VDI was null"); + } + snapshot.destroy(conn); + s_logger.debug("Successfully destroyed snapshotUuid: " + snapshotUuid + " on primary storage"); + return true; + } catch (XenAPIException e) { + String msg = "Destroy snapshotUuid: " + snapshotUuid + " on primary storage failed due to " + e.toString(); + s_logger.error(msg, e); + } catch (Exception e) { + String msg = "Destroy snapshotUuid: " + snapshotUuid + " on primary storage failed due to " + e.getMessage(); + s_logger.warn(msg, e); + } + + return false; + } + + protected String deleteSnapshotBackup(Long dcId, Long accountId, Long volumeId, String secondaryStorageMountPath, String backupUUID, String childUUID, Boolean isISCSI) { + + // If anybody modifies the formatting below again, I'll skin them + String result = callHostPlugin("vmopsSnapshot", "deleteSnapshotBackup", "backupUUID", backupUUID, "childUUID", childUUID, "dcId", dcId.toString(), "accountId", accountId.toString(), + "volumeId", volumeId.toString(), "secondaryStorageMountPath", secondaryStorageMountPath, "isISCSI", isISCSI.toString()); + + return result; + } + + protected String deleteSnapshotsDir(Long dcId, Long accountId, Long volumeId, String secondaryStorageMountPath) { + // If anybody modifies the formatting below again, I'll skin them + String result = callHostPlugin("vmopsSnapshot", "deleteSnapshotsDir", "dcId", dcId.toString(), "accountId", accountId.toString(), "volumeId", volumeId.toString(), + "secondaryStorageMountPath", secondaryStorageMountPath); + + return result; + } + + // If anybody messes up with the formatting, I'll skin them + protected Pair createVHDFromSnapshot(String primaryStorageNameLabel, Long dcId, Long accountId, Long volumeId, String secondaryStoragePoolURL, + String backedUpSnapshotUuid, String templatePath, String templateDownloadFolder) throws XenAPIException, IOException, XmlRpcException, InternalErrorException, + URISyntaxException { + // Return values + String details = null; + Connection conn = getConnection(); + SR primaryStorageSR = getSRByNameLabelandHost(primaryStorageNameLabel); + if (primaryStorageSR == null) { + throw new InternalErrorException("Could not create volume from snapshot " + "because the primary Storage SR could not be created from the name label: " + + primaryStorageNameLabel); + } + + Boolean isISCSI = SRType.LVMOISCSI.equals(primaryStorageSR.getType(conn)); + + // Get the absolute path of the template on the secondary storage. + URI uri = new URI(secondaryStoragePoolURL); + String secondaryStorageMountPath = uri.getHost() + ":" + uri.getPath(); + VHDInfo vhdInfo = null; + if (secondaryStorageMountPath == null) { + details = " because the URL passed: " + secondaryStoragePoolURL + " is invalid."; + } else { + vhdInfo = createVHDFromSnapshot(dcId, accountId, volumeId, secondaryStorageMountPath, backedUpSnapshotUuid, templatePath, templateDownloadFolder, isISCSI); + if (vhdInfo == null) { + details = " because the vmops plugin on XenServer failed at some point"; + } + } + + return new Pair(vhdInfo, details); + } + + protected VHDInfo createVHDFromSnapshot(Long dcId, Long accountId, Long volumeId, String secondaryStorageMountPath, String backedUpSnapshotUuid, String templatePath, + String templateDownloadFolder, Boolean isISCSI) { + String vdiUUID = null; + + String failureString = "Could not create volume from " + backedUpSnapshotUuid; + templatePath = (templatePath == null) ? "" : templatePath; + String results = callHostPlugin("vmopsSnapshot", "createVolumeFromSnapshot", "dcId", dcId.toString(), "accountId", accountId.toString(), "volumeId", volumeId.toString(), + "secondaryStorageMountPath", secondaryStorageMountPath, "backedUpSnapshotUuid", backedUpSnapshotUuid, "templatePath", templatePath, "templateDownloadFolder", + templateDownloadFolder, "isISCSI", isISCSI.toString()); + + if (results == null || results.isEmpty()) { + // Command threw an exception which has already been logged. + return null; + } + String[] tmp = results.split("#"); + String status = tmp[0]; + vdiUUID = tmp[1]; + Long virtualSizeInMB = 0L; + if (tmp.length == 3) { + virtualSizeInMB = Long.valueOf(tmp[2]); + } + // status == "1" if and only if vdiUUID != null + // So we don't rely on status value but return vdiUUID as an indicator + // of success. + + if (status != null && status.equalsIgnoreCase("1") && vdiUUID != null) { + s_logger.debug("Successfully created vhd file with all data on secondary storage : " + vdiUUID); + } else { + s_logger.debug(failureString + ". Failed with status " + status + " with vdiUuid " + vdiUUID); + } + return new VHDInfo(vdiUUID, virtualSizeInMB * MB); + + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + disconnected(); + return true; + } + + @Override + public String getName() { + return _name; + } + + @Override + public IAgentControl getAgentControl() { + return _agentControl; + } + + @Override + public void setAgentControl(IAgentControl agentControl) { + _agentControl = agentControl; + } + + + + protected Answer execute(PoolEjectCommand cmd) { + Connection conn = getConnection(); + String hostuuid = cmd.getHostuuid(); + try { + Host host = Host.getByUuid(conn, hostuuid); + // remove all tags cloud stack add before eject + Host.Record hr = host.getRecord(conn); + Iterator it = hr.tags.iterator(); + while (it.hasNext()) { + String tag = it.next(); + if (tag.startsWith("vmops-version-")) { + it.remove(); + } + } + // eject from pool + Pool.eject(conn, host); + return new Answer(cmd); + } catch (XenAPIException e) { + String msg = "Unable to eject host " + _host.uuid + " due to " + e.toString(); + s_logger.warn(msg, e); + return new Answer(cmd, false, msg); + } catch (Exception e) { + s_logger.warn("Unable to eject host " + _host.uuid, e); + String msg = "Unable to eject host " + _host.uuid + " due to " + e.getMessage(); + s_logger.warn(msg, e); + return new Answer(cmd, false, msg); + } + + } + + protected class Nic { + public Network n; + public Network.Record nr; + public PIF p; + public PIF.Record pr; + + public Nic(Network n, Network.Record nr, PIF p, PIF.Record pr) { + this.n = n; + this.nr = nr; + this.p = p; + this.pr = pr; + } + } + + // A list of UUIDs that are gathered from the XenServer when + // the resource first connects to XenServer. These UUIDs do + // not change over time. + protected class XenServerHost { + public String uuid; + public String ip; + public String publicNetwork; + public String privateNetwork; + public String linkLocalNetwork; + public String storageNetwork1; + public String storageNetwork2; + public String guestNetwork; + public String guestPif; + public String publicPif; + public String privatePif; + public String storagePif1; + public String storagePif2; + public String pool; + } + + private class VHDInfo { + private final String uuid; + private final Long virtualSize; + + public VHDInfo(String uuid, Long virtualSize) { + this.uuid = uuid; + this.virtualSize = virtualSize; + } + + /** + * @return the uuid + */ + public String getUuid() { + return uuid; + } + + /** + * @return the virtualSize + */ + public Long getVirtualSize() { + return virtualSize; + } + } +} diff --git a/core/src/com/cloud/hypervisor/xen/resource/XcpServerResource.java b/core/src/com/cloud/hypervisor/xen/resource/XcpServerResource.java new file mode 100644 index 00000000000..090ad36fc78 --- /dev/null +++ b/core/src/com/cloud/hypervisor/xen/resource/XcpServerResource.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.hypervisor.xen.resource; + +import javax.ejb.Local; + +import com.cloud.resource.ServerResource; + +@Local(value=ServerResource.class) +public class XcpServerResource extends CitrixResourceBase { + public XcpServerResource() { + super(); + } +} diff --git a/core/src/com/cloud/hypervisor/xen/resource/XenServerConnectionPool.java b/core/src/com/cloud/hypervisor/xen/resource/XenServerConnectionPool.java new file mode 100644 index 00000000000..84614d8f888 --- /dev/null +++ b/core/src/com/cloud/hypervisor/xen/resource/XenServerConnectionPool.java @@ -0,0 +1,637 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.hypervisor.xen.resource; + +import java.net.MalformedURLException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.apache.log4j.Logger; +import org.apache.xmlrpc.XmlRpcException; + +import com.cloud.utils.exception.CloudRuntimeException; +import com.xensource.xenapi.APIVersion; +import com.xensource.xenapi.Connection; +import com.xensource.xenapi.Host; +import com.xensource.xenapi.Pool; +import com.xensource.xenapi.Session; +import com.xensource.xenapi.Types; +import com.xensource.xenapi.Types.XenAPIException; + +public class XenServerConnectionPool { + private static final Logger s_logger = Logger.getLogger(XenServerConnectionPool.class); + + protected HashMap _conns = new HashMap(); + protected HashMap _infos = new HashMap(); + + protected int _retries; + protected int _interval; + + protected XenServerConnectionPool() { + _retries = 10; + _interval = 3; + } + + public synchronized void switchMaster(String lipaddr, String poolUuid, Connection conn, Host host, String username, String password, int wait) throws XmlRpcException, XenAPIException { + String ipaddr = host.getAddress(conn); + PoolSyncDB(conn); + s_logger.debug("Designating the new master to " + ipaddr); + Pool.designateNewMaster(conn, host); + XenServerConnection slaveConn = null; + XenServerConnection masterConn = null; + int retry = 30; + for (int i = 0; i < retry; i++) { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + } + try { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Logging on as the slave to " + lipaddr); + } + slaveConn = null; + masterConn = null; + URL slaveUrl = null; + URL masterUrl = null; + Session slaveSession = null; + + slaveUrl = new URL("http://" + lipaddr); + slaveConn = new XenServerConnection(slaveUrl, username, password, _retries, _interval, 10); + slaveSession = Session.slaveLocalLoginWithPassword(slaveConn, username, password); + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Slave logon successful. session= " + slaveSession); + } + + Pool.Record pr = getPoolRecord(slaveConn); + Host master = pr.master; + String ma = master.getAddress(slaveConn); + if( !ma.trim().equals(ipaddr.trim()) ) { + continue; + } + Session.localLogout(slaveConn); + + masterUrl = new URL("http://" + ipaddr); + masterConn = new XenServerConnection(masterUrl, username, password, _retries, _interval, wait); + Session.loginWithPassword(masterConn, username, + password, + APIVersion.latest().toString()); + cleanup(poolUuid); + Pool.recoverSlaves(masterConn); + PoolSyncDB(masterConn); + return; + } catch (Types.HostIsSlave e) { + s_logger.debug("HostIsSlaveException: Still waiting for the conversion to the master"); + } catch (XmlRpcException e) { + s_logger.debug("XmlRpcException: Still waiting for the conversion to the master " + e.getMessage()); + } catch (Exception e) { + s_logger.debug("Exception: Still waiting for the conversion to the master" + e.getMessage()); + } finally { + if (masterConn != null) { + try { + Session.logout(masterConn); + } catch (Exception e) { + s_logger.debug("Unable to log out of session: " + e.getMessage()); + } + masterConn.dispose(); + masterConn = null; + } + } + + } + + throw new CloudRuntimeException("Unable to logon to the new master after " + retry + " retries"); + } + + protected synchronized void cleanup(String poolUuid) { + ConnectionInfo info = _infos.remove(poolUuid); + if (info == null) { + s_logger.debug("Unable to find any information for pool " + poolUuid); + return; + } + + for (Member member : info.refs.values()) { + _conns.remove(member.uuid); + } + + if (info.conn != null) { + try { + s_logger.debug("Logging out of session " + info.conn.getSessionReference()); + + Session.logout(info.conn); + } catch (XenAPIException e) { + s_logger.debug("Unable to logout of the session"); + } catch (XmlRpcException e) { + s_logger.debug("Unable to logout of the session"); + } + info.conn.dispose(); + } + s_logger.debug("Session is cleaned up"); + } + + protected synchronized void cleanup(String poolUuid, ConnectionInfo info) { + ConnectionInfo info2 = _infos.get(poolUuid); + if (info != info2) { + s_logger.debug("Session " + info.conn.getSessionReference() + " is already logged out."); + return; + } + + cleanup(poolUuid); + } + + public synchronized void disconnect(String uuid, String poolUuid) { + Connection conn = _conns.remove(uuid); + if (conn == null) { + return; + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Logging out of " + conn.getSessionReference() + " for host " + uuid); + } + + conn.dispose(); + + ConnectionInfo info = _infos.get(poolUuid); + if (info == null) { + return; + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Connection for pool " + poolUuid + " found. session=" + info.conn.getSessionReference()); + } + + info.refs.remove(uuid); + if (info.refs.size() == 0) { + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Logging out of the session " + info.conn.getSessionReference()); + } + _infos.remove(poolUuid); + try { + Session.logout(info.conn); + info.conn.dispose(); + } catch (Exception e) { + s_logger.debug("Logout has a problem " + e.getMessage()); + } + info.conn = null; + } + } + + public static void logout(Connection conn) { + try { + s_logger.debug("Logging out of the session " + conn.getSessionReference()); + Session.logout(conn); + } catch (Exception e) { + s_logger.debug("Logout has problem " + e.getMessage()); + } finally { + conn.dispose(); + } + } + + public Connection connect(String urlString, String username, String password, int wait) { + return connect(null, urlString, username, password, wait); + } + + protected ConnectionInfo getConnectionInfo(String poolUuid) { + synchronized(_infos) { + return _infos.get(poolUuid); + } + } + + protected XenServerConnection getConnection(String hostUuid) { + synchronized(_conns) { + return _conns.get(hostUuid); + } + } + + static void PoolSyncDB(Connection conn) { + try{ + Set hosts = Host.getAll(conn); + for(Host host : hosts) { + try { + host.enable(conn); + } catch (Exception e) { + } + } + } catch (Exception e) { + s_logger.debug("Enbale host failed due to " + e.getMessage() + e.toString()); + } + try{ + Pool.syncDatabase(conn); + } catch (Exception e) { + s_logger.debug("Sync Database failed due to " + e.getMessage() + e.toString()); + } + + + } + + protected synchronized URL ensurePoolIntegrity(Connection conn, Host master, Pool.Record poolr, String ipAddress, String username, String password, int wait) throws XenAPIException, XmlRpcException { + if (!ipAddress.equals(master.getAddress(conn))) { + return null; // Doesn't think it is the master anyways. + } + + String poolUuid = poolr.uuid; + ConnectionInfo info = _infos.get(poolUuid); + if (info != null) { + Connection poolConn = info.conn; + if (poolConn != null) { + Pool.recoverSlaves(poolConn); + PoolSyncDB(poolConn); + return info.masterUrl; + } + } + + Set slaves = Host.getAll(conn); + HashMap count = new HashMap(slaves.size()); + for (Host slave : slaves) { + String slaveAddress = slave.getAddress(conn); + Connection slaveConn = null; + try { + slaveConn = new Connection(new URL("http://" + slaveAddress), wait); + if (slaveConn != null) { + Session slaveSession = Session.slaveLocalLoginWithPassword(slaveConn, username, password); + Pool.Record slavePoolr = getPoolRecord(slaveConn); + String possibleMaster = slavePoolr.master.getAddress(slaveConn); + Integer c = count.get(possibleMaster); + if (c == null) { + c = 1; + } else { + c++; + } + count.put(possibleMaster, c); + try { + slaveSession.logout(slaveConn); + } catch (Exception e) { + s_logger.debug("client session logout: " + e.getMessage()); + } + slaveConn.dispose(); + } + } catch (MalformedURLException e) { + throw new CloudRuntimeException("Bad URL" + slaveAddress, e); + } catch (Exception e) { + s_logger.debug("Unable to login to slave " + slaveAddress + " error " + e.getMessage()); + } finally { + if (slaveConn != null) { + slaveConn.dispose(); + } + } + } + + Iterator> it = count.entrySet().iterator(); + + Map.Entry newMaster = it.next(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + + if (newMaster.getValue() < entry.getValue()) { + newMaster = entry; + } + } + + String newMasterAddress = newMaster.getKey(); + if (count.size() > 1 && !ipAddress.equals(newMasterAddress)) { + s_logger.debug("Asking the correct master to recover the slaves: " + newMasterAddress); + + URL newMasterUrl = null; + try { + newMasterUrl = new URL("http://" + newMasterAddress); + } catch (MalformedURLException e) { + throw new CloudRuntimeException("Unable to get url from " + newMasterAddress, e); + } + + Connection newMasterConn = new Connection(newMasterUrl, wait); + try { + Session.loginWithPassword(newMasterConn, username, password, APIVersion.latest().toString()); + Pool.recoverSlaves(newMasterConn); + PoolSyncDB(newMasterConn); + } catch (Exception e) { + throw new CloudRuntimeException("Unable to login to the real master at " + newMaster.getKey()); + } finally { + try { + Session.logout(newMasterConn); + } catch (Exception e) { + s_logger.debug("Unable to logout of the session: " + e.getMessage()); + } + newMasterConn.dispose(); + } + + return newMasterUrl; + } + + return null; + } + + public synchronized Connection connect(String hostUuid, String ipAddress, String username, String password, int wait) { + XenServerConnection masterConn = null; + if (hostUuid != null) { // Let's see if it is an existing connection. + masterConn = _conns.get(hostUuid); + if (masterConn != null) { + return masterConn; + } + } + + XenServerConnection slaveConn = null; + URL slaveUrl = null; + URL masterUrl = null; + Session slaveSession = null; + Session masterSession = null; + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Creating connection to " + ipAddress); + } + + // Well, it's not already in the existing connection list. + // Let's login and see what this connection should be. + // You might think this is slow. Why not cache the pool uuid + // you say? Well, this doesn't happen very often. + try { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Logging on as the slave to " + ipAddress); + } + slaveUrl = new URL("http://" + ipAddress); + slaveConn = new XenServerConnection(slaveUrl, username, password, _retries, _interval, 10); + slaveSession = Session.slaveLocalLoginWithPassword(slaveConn, username, password); + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Slave logon successful. session= " + slaveSession); + } + + try { + Pool.Record pr = getPoolRecord(slaveConn); + Host master = pr.master; + String ma = master.getAddress(slaveConn); + masterUrl = new URL("http://" + ma); + s_logger.debug("Logging on as the master to " + ma); + masterConn = new XenServerConnection(masterUrl, username, password, _retries, _interval, wait); + masterSession = Session.loginWithPassword(masterConn, username, password, APIVersion.latest().toString()); + + URL realMasterUrl = ensurePoolIntegrity(masterConn, master, pr, ipAddress, username, password, wait); + if (realMasterUrl != null) { + s_logger.debug("The real master url is at " + realMasterUrl); + masterUrl = realMasterUrl; + masterConn.dispose(); + masterConn = new XenServerConnection(masterUrl, username, password, _retries, _interval, wait); + masterSession = Session.loginWithPassword(masterConn, username, password, APIVersion.latest().toString()); + + } + } catch (XmlRpcException e) { + Throwable c = e.getCause(); + if (c == null || (!(c instanceof SocketException) && !(c instanceof SocketTimeoutException))) { + s_logger.warn("Unable to connect to " + masterUrl, e); + throw e; + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Unable to connect to the " + masterUrl + ". Attempting switch to master"); + } + Pool.emergencyTransitionToMaster(slaveConn); + Pool.recoverSlaves(slaveConn); + + s_logger.info("Successfully converted to the master: " + ipAddress); + + masterUrl = slaveUrl; + masterConn = new XenServerConnection(masterUrl, username, password, _retries, _interval, wait); + masterSession = Session.loginWithPassword(masterConn, username, password, APIVersion.latest().toString()); + } + + if (slaveSession != null) { + s_logger.debug("Login to the master is successful. Signing out of the slave connection: " + slaveSession); + try { + Session.localLogout(slaveConn); + } catch (Exception e) { + s_logger.debug("Unable to logout of slave session " + slaveSession); + } + slaveConn.dispose(); + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Successfully logged on to the master. Session=" + masterConn.getSessionReference()); + } + if (hostUuid == null) { + s_logger.debug("Returning now. Client is responsible for logging out of " + masterConn.getSessionReference()); + return masterConn; + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Logon is successfully. Let's see if we have other hosts logged onto the same master at " + masterUrl); + } + + Pool.Record poolr = getPoolRecord(masterConn); + String poolUuid = poolr.uuid; + + ConnectionInfo info = null; + info = _infos.get(poolUuid); + if (info != null && info.conn != null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("The pool already has a master connection. Session=" + info.conn.getSessionReference()); + } + try { + s_logger.debug("Logging out of our own session: " + masterConn.getSessionReference()); + Session.logout(masterConn); + masterConn.dispose(); + } catch (Exception e) { + s_logger.debug("Caught Exception while logging on but pushing on...." + e.getMessage()); + } + masterConn = new XenServerConnection(info.conn); + } else { + if (s_logger.isDebugEnabled()) { + s_logger.debug("This is the very first connection"); + } + info = new ConnectionInfo(); + info.conn = masterConn; + info.masterUrl = masterUrl; + info.refs = new HashMap(); + _infos.put(poolUuid, info); + masterConn.setInfo(poolUuid, info); + if (s_logger.isDebugEnabled()) { + s_logger.debug("Pool " + poolUuid + " is matched with session " + info.conn.getSessionReference()); + } + } + + s_logger.debug("Added a reference for host " + hostUuid + " to session " + masterConn.getSessionReference() + " in pool " + poolUuid); + info.refs.put(hostUuid, new Member(slaveUrl, hostUuid, username, password)); + _conns.put(hostUuid, masterConn); + + s_logger.info("Connection made to " + ipAddress + " for host " + hostUuid + ". Pool Uuid is " + poolUuid); + + return masterConn; + } catch (XenAPIException e) { + s_logger.warn("Unable to make a connection to the server " + ipAddress, e); + throw new CloudRuntimeException("Unable to make a connection to the server " + ipAddress, e); + } catch (XmlRpcException e) { + s_logger.warn("Unable to make a connection to the server " + ipAddress, e); + throw new CloudRuntimeException("Unable to make a connection to the server " + ipAddress, e); + } catch (MalformedURLException e) { + throw new CloudRuntimeException("How can we get a malformed exception for this " + ipAddress, e); + } + } + + + + protected Pool.Record getPoolRecord(Connection conn) throws XmlRpcException, XenAPIException { + Map pools = Pool.getAllRecords(conn); + assert pools.size() == 1 : "Pool size is not one....hmmm....wth? " + pools.size(); + + return pools.values().iterator().next(); + } + + private static final XenServerConnectionPool s_instance = new XenServerConnectionPool(); + public static XenServerConnectionPool getInstance() { + return s_instance; + } + + protected class ConnectionInfo { + public URL masterUrl; + public XenServerConnection conn; + public HashMap refs = new HashMap(); + } + + protected class Member { + public URL url; + public String uuid; + public String username; + public String password; + + public Member(URL url, String uuid, String username, String password) { + this.url = url; + this.uuid = uuid; + this.username = username; + this.password = password; + } + } + + public class XenServerConnection extends Connection { + long _interval; + int _retries; + String _username; + String _password; + URL _url; + ConnectionInfo _info; + String _poolUuid; + + public XenServerConnection(URL url, String username, String password, int retries, int interval, int wait) { + super(url, wait); + _url = url; + _retries = retries; + _username = username; + _password = password; + _interval = (long)interval * 1000; + } + + public XenServerConnection(XenServerConnection that) { + super(that._url, that.getSessionReference(), that._wait); + this._url = that._url; + this._retries = that._retries; + this._username = that._username; + this._password = that._password; + this._interval = that._interval; + this._info = that._info; + this._poolUuid = that._poolUuid; + + } + + public void setInfo(String poolUuid, ConnectionInfo info) { + this._poolUuid = poolUuid; + this._info = info; + } + + public int getWaitTimeout() { + return _wait; + } + + @Override + protected Map dispatch(String method_call, Object[] method_params) throws XmlRpcException, XenAPIException { + if (method_call.equals("session.login_with_password") || method_call.equals("session.slave_local_login_with_password") || method_call.equals("session.logout")) { + int retries = 0; + while (retries++ < _retries) { + try { + return super.dispatch(method_call, method_params); + } catch (XmlRpcException e) { + Throwable cause = e.getCause(); + if (cause == null || !(cause instanceof SocketException)) { + throw e; + } + if (retries >= _retries) { + throw e; + } + s_logger.debug("Unable to login...retrying " + retries); + } + try { + Thread.sleep(_interval); + } catch(InterruptedException e) { + s_logger.debug("Man....I was just getting comfortable there....who woke me up?"); + } + } + } + + int retries = 0; + while (retries++ < _retries) { + try { + return super.dispatch(method_call, method_params); + } catch (Types.SessionInvalid e) { + if (retries >= _retries) { + if (_poolUuid != null) { + cleanup(_poolUuid, _info); + } + throw e; + } + s_logger.debug("Session is invalid. Reconnecting...retry=" + retries); + } catch (XmlRpcException e) { + if (retries >= _retries) { + if (_poolUuid != null) { + cleanup(_poolUuid, _info); + } + throw e; + } + Throwable cause = e.getCause(); + if (cause == null || !(cause instanceof SocketException)) { + if (_poolUuid != null) { + cleanup(_poolUuid, _info); + } + throw e; + } + s_logger.debug("Connection couldn't be made. Reconnecting....retry=" + retries); + } catch (Types.HostIsSlave e) { + if (_poolUuid != null) { + cleanup(_poolUuid, _info); + } + throw e; + } + + try { + Thread.sleep(_interval); + } catch (InterruptedException e) { + s_logger.info("Who woke me from my slumber?"); + } + + Session session = Session.loginWithPassword(this, _username, _password, APIVersion.latest().toString()); + method_params[0] = getSessionReference(); + } + assert false : "We should never get here"; + if (_poolUuid != null) { + cleanup(_poolUuid, _info); + } + throw new CloudRuntimeException("After " + _retries + " retries, we cannot contact the host "); + } + } +} diff --git a/core/src/com/cloud/info/ConsoleProxyConnectionInfo.java b/core/src/com/cloud/info/ConsoleProxyConnectionInfo.java new file mode 100644 index 00000000000..1b1ed267ce3 --- /dev/null +++ b/core/src/com/cloud/info/ConsoleProxyConnectionInfo.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.info; + +public class ConsoleProxyConnectionInfo { + public int id; + public String host; + public int port; + public String tag; + public long createTime; + public long lastUsedTime; + + public ConsoleProxyConnectionInfo() { + } +} diff --git a/core/src/com/cloud/info/ConsoleProxyInfo.java b/core/src/com/cloud/info/ConsoleProxyInfo.java new file mode 100644 index 00000000000..1e310ac9764 --- /dev/null +++ b/core/src/com/cloud/info/ConsoleProxyInfo.java @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.info; + +public class ConsoleProxyInfo { + + private boolean sslEnabled; + private String proxyAddress; + private int proxyPort; + private String proxyImageUrl; + private int proxyUrlPort = 8000; + + public ConsoleProxyInfo(int proxyUrlPort) { + this.proxyUrlPort = proxyUrlPort; + } + + public ConsoleProxyInfo(boolean sslEnabled, String proxyIpAddress, int port, int proxyUrlPort) { + this.sslEnabled = sslEnabled; + + if(sslEnabled) { + StringBuffer sb = new StringBuffer(proxyIpAddress); + for(int i = 0; i < sb.length(); i++) + if(sb.charAt(i) == '.') + sb.setCharAt(i, '-'); + sb.append(".realhostip.com"); + proxyAddress = sb.toString(); + proxyPort = port; + this.proxyUrlPort = proxyUrlPort; + + proxyImageUrl = "https://" + proxyAddress; + if(proxyUrlPort != 443) + proxyImageUrl += ":" + this.proxyUrlPort; + } else { + proxyAddress = proxyIpAddress; + proxyPort = port; + this.proxyUrlPort = proxyUrlPort; + + proxyImageUrl = "http://" + proxyAddress; + if(proxyUrlPort != 80) + proxyImageUrl += ":" + proxyUrlPort; + } + } + + public String getProxyAddress() { + return proxyAddress; + } + + public void setProxyAddress(String proxyAddress) { + this.proxyAddress = proxyAddress; + } + + public int getProxyPort() { + return proxyPort; + } + + public void setProxyPort(int proxyPort) { + this.proxyPort = proxyPort; + } + + public String getProxyImageUrl() { + return proxyImageUrl; + } + + public void setProxyImageUrl(String proxyImageUrl) { + this.proxyImageUrl = proxyImageUrl; + } + + public boolean isSslEnabled() { + return sslEnabled; + } + + public void setSslEnabled(boolean sslEnabled) { + this.sslEnabled = sslEnabled; + } +} diff --git a/core/src/com/cloud/info/ConsoleProxyLoadInfo.java b/core/src/com/cloud/info/ConsoleProxyLoadInfo.java new file mode 100644 index 00000000000..c4e7fa364f9 --- /dev/null +++ b/core/src/com/cloud/info/ConsoleProxyLoadInfo.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.info; + +public class ConsoleProxyLoadInfo { + + private long id; + private String name; + private int count; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +} diff --git a/core/src/com/cloud/info/ConsoleProxyStatus.java b/core/src/com/cloud/info/ConsoleProxyStatus.java new file mode 100644 index 00000000000..0f628283f9a --- /dev/null +++ b/core/src/com/cloud/info/ConsoleProxyStatus.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.info; + +public class ConsoleProxyStatus { + private ConsoleProxyConnectionInfo[] connections; + + public ConsoleProxyStatus() { + } + + public ConsoleProxyConnectionInfo[] getConnections() { + return connections; + } +} diff --git a/core/src/com/cloud/info/RunningHostCountInfo.java b/core/src/com/cloud/info/RunningHostCountInfo.java new file mode 100644 index 00000000000..35512e662ae --- /dev/null +++ b/core/src/com/cloud/info/RunningHostCountInfo.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.info; + +public class RunningHostCountInfo { + + private long dcId; + private String hostType; + private int count; + + public long getDcId() { + return dcId; + } + + public void setDcId(long dcId) { + this.dcId = dcId; + } + + public String getHostType() { + return hostType; + } + + public void setHostType(String hostType) { + this.hostType = hostType; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +} diff --git a/core/src/com/cloud/info/RunningHostInfoAgregator.java b/core/src/com/cloud/info/RunningHostInfoAgregator.java new file mode 100644 index 00000000000..ab1258b6c43 --- /dev/null +++ b/core/src/com/cloud/info/RunningHostInfoAgregator.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.info; + +import java.util.HashMap; +import java.util.Map; + +import com.cloud.host.Host; + +public class RunningHostInfoAgregator { + + public static class ZoneHostInfo { + public static int ROUTING_HOST_MASK = 2; + public static int STORAGE_HOST_MASK = 4; + public static int ALL_HOST_MASK = ROUTING_HOST_MASK | STORAGE_HOST_MASK; + + private long dcId; + + // (1 << 1) : at least one routing host is running in the zone + // (1 << 2) : at least one storage host is running in the zone + private int flags = 0; + + public long getDcId() { + return dcId; + } + + public void setDcId(long dcId) { + this.dcId = dcId; + } + + public void setFlag(int flagMask) { + flags |= flagMask; + } + + public int getFlags() { + return flags; + } + } + + private Map zoneHostInfoMap = new HashMap(); + + public RunningHostInfoAgregator() { + } + + public void aggregate(RunningHostCountInfo countInfo) { + if(countInfo.getCount() > 0) { + ZoneHostInfo zoneInfo = getZoneHostInfo(countInfo.getDcId()); + + Host.Type type = Enum.valueOf(Host.Type.class, countInfo.getHostType()); + if(type == Host.Type.Routing) { + zoneInfo.setFlag(ZoneHostInfo.ROUTING_HOST_MASK); + } else if(type == Host.Type.Storage || type == Host.Type.SecondaryStorage) { + zoneInfo.setFlag(ZoneHostInfo.STORAGE_HOST_MASK); + } + } + } + + public Map getZoneHostInfoMap() { + return zoneHostInfoMap; + } + + private ZoneHostInfo getZoneHostInfo(long dcId) { + if(zoneHostInfoMap.containsKey(dcId)) + return zoneHostInfoMap.get(dcId); + + ZoneHostInfo info = new ZoneHostInfo(); + info.setDcId(dcId); + zoneHostInfoMap.put(dcId, info); + return info; + } +} diff --git a/core/src/com/cloud/info/SecStorageVmLoadInfo.java b/core/src/com/cloud/info/SecStorageVmLoadInfo.java new file mode 100644 index 00000000000..1271d330df5 --- /dev/null +++ b/core/src/com/cloud/info/SecStorageVmLoadInfo.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.info; + +public class SecStorageVmLoadInfo { + + private long id; + private String name; + private int count; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +} diff --git a/core/src/com/cloud/maid/StackMaid.java b/core/src/com/cloud/maid/StackMaid.java new file mode 100644 index 00000000000..b89e97e01b6 --- /dev/null +++ b/core/src/com/cloud/maid/StackMaid.java @@ -0,0 +1,123 @@ +package com.cloud.maid; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.log4j.Logger; + +import com.cloud.maid.dao.StackMaidDao; +import com.cloud.maid.dao.StackMaidDaoImpl; +import com.cloud.serializer.SerializerHelper; +import com.cloud.utils.ActionDelegate; + +public class StackMaid { + protected final static Logger s_logger = Logger.getLogger(StackMaid.class); + + private static ThreadLocal threadMaid = new ThreadLocal(); + + private static long msid_setby_manager = 0; + + // tired of using component locator as I want this class to be accessed freely + private StackMaidDao maidDao = new StackMaidDaoImpl(); + private int currentSeq = 0; + private Map context = new HashMap(); + + public static void init(long msid) { + msid_setby_manager = msid; + } + + public static StackMaid current() { + StackMaid maid = threadMaid.get(); + if(maid == null) { + maid = new StackMaid(); + threadMaid.set(maid); + } + return maid; + } + + public void registerContext(String key, Object contextObject) { + assert(!context.containsKey(key)) : "Context key has already been registered"; + context.put(key, contextObject); + } + + public Object getContext(String key) { + return context.get(key); + } + + public int push(String delegateClzName, Object context) { + assert(msid_setby_manager != 0) : "Fatal, make sure StackMaidManager is loaded"; + if(msid_setby_manager == 0) + s_logger.error("Fatal, make sure StackMaidManager is loaded"); + + return push(msid_setby_manager, delegateClzName, context); + } + + public int push(long currentMsid, String delegateClzName, Object context) { + int savePoint = currentSeq; + maidDao.pushCleanupDelegate(currentMsid, currentSeq++, delegateClzName, context); + return savePoint; + } + + public void pop(int savePoint) { + assert(msid_setby_manager != 0) : "Fatal, make sure StackMaidManager is loaded"; + if(msid_setby_manager == 0) + s_logger.error("Fatal, make sure StackMaidManager is loaded"); + + pop(msid_setby_manager, savePoint); + } + + /** + * must be called within thread context + * @param currentMsid + */ + public void pop(long currentMsid, int savePoint) { + while(currentSeq > savePoint) { + maidDao.popCleanupDelegate(currentMsid); + currentSeq--; + } + } + + public void exitCleanup() { +/* + assert(msid_setby_manager != 0) : "Fatal, make sure StackMaidManager is loaded"; + if(msid_setby_manager == 0) + s_logger.error("Fatal, make sure StackMaidManager is loaded"); +*/ + + exitCleanup(msid_setby_manager); + } + + public void exitCleanup(long currentMsid) { + if(currentSeq > 0) { + StackMaidVO maid = null; + while((maid = maidDao.popCleanupDelegate(currentMsid)) != null) { + doCleanup(maid); + } + currentSeq = 0; + } + + context.clear(); + } + + public static void doCleanup(StackMaidVO maid) { + if(maid.getDelegate() != null) { + try { + Class clz = Class.forName(maid.getDelegate()); + Object delegate = clz.newInstance(); + if(delegate instanceof ActionDelegate) { + ((ActionDelegate)delegate).action(SerializerHelper.fromSerializedString(maid.getContext())); + } + } catch (final ClassNotFoundException e) { + s_logger.error("Unable to load StackMaid delegate class: " + maid.getDelegate(), e); + } catch (final SecurityException e) { + s_logger.error("Security excetion when loading resource: " + maid.getDelegate()); + } catch (final IllegalArgumentException e) { + s_logger.error("Illegal argument excetion when loading resource: " + maid.getDelegate()); + } catch (final InstantiationException e) { + s_logger.error("Instantiation excetion when loading resource: " + maid.getDelegate()); + } catch (final IllegalAccessException e) { + s_logger.error("Illegal access exception when loading resource: " + maid.getDelegate()); + } + } + } +} diff --git a/core/src/com/cloud/maid/StackMaidVO.java b/core/src/com/cloud/maid/StackMaidVO.java new file mode 100644 index 00000000000..5b8c145c8a1 --- /dev/null +++ b/core/src/com/cloud/maid/StackMaidVO.java @@ -0,0 +1,99 @@ +package com.cloud.maid; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="stack_maid") +public class StackMaidVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="msid") + private long msid; + + @Column(name="thread_id") + private long threadId; + + @Column(name="seq") + private int seq; + + @Column(name="cleanup_delegate", length=128) + private String delegate; + + @Column(name="cleanup_context", length=65535) + private String context; + + @Column(name=GenericDao.CREATED_COLUMN) + private Date created; + + public StackMaidVO() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public long getMsid() { + return msid; + } + + public void setMsid(long msid) { + this.msid = msid; + } + + public long getThreadId() { + return threadId; + } + + public void setThreadId(long threadId) { + this.threadId = threadId; + } + + public int getSeq() { + return seq; + } + + public void setSeq(int seq) { + this.seq = seq; + } + + public String getDelegate() { + return delegate; + } + + public void setDelegate(String delegate) { + this.delegate = delegate; + } + + public String getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public Date getCreated() { + return this.created; + } + + public void setCreated(Date created) { + this.created = created; + } +} diff --git a/core/src/com/cloud/maid/dao/StackMaidDao.java b/core/src/com/cloud/maid/dao/StackMaidDao.java new file mode 100644 index 00000000000..c03e0cca138 --- /dev/null +++ b/core/src/com/cloud/maid/dao/StackMaidDao.java @@ -0,0 +1,16 @@ +package com.cloud.maid.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.maid.StackMaidVO; +import com.cloud.utils.db.GenericDao; + +public interface StackMaidDao extends GenericDao { + public void pushCleanupDelegate(long msid, int seq, String delegateClzName, Object context); + public StackMaidVO popCleanupDelegate(long msid); + public void clearStack(long msid); + + public List listLeftoversByMsid(long msid); + public List listLeftoversByCutTime(Date cutTime); +} diff --git a/core/src/com/cloud/maid/dao/StackMaidDaoImpl.java b/core/src/com/cloud/maid/dao/StackMaidDaoImpl.java new file mode 100644 index 00000000000..e57afb51724 --- /dev/null +++ b/core/src/com/cloud/maid/dao/StackMaidDaoImpl.java @@ -0,0 +1,126 @@ +package com.cloud.maid.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.maid.StackMaidVO; +import com.cloud.serializer.SerializerHelper; +import com.cloud.utils.DateUtil; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value = { StackMaidDao.class }) +public class StackMaidDaoImpl extends GenericDaoBase implements StackMaidDao { + private static final Logger s_logger = Logger.getLogger(StackMaidDaoImpl.class.getName()); + + private SearchBuilder popSearch; + private SearchBuilder clearSearch; + + public StackMaidDaoImpl() { + popSearch = createSearchBuilder(); + popSearch.and("msid", popSearch.entity().getMsid(), SearchCriteria.Op.EQ); + popSearch.and("threadId", popSearch.entity().getThreadId(), SearchCriteria.Op.EQ); + + clearSearch = createSearchBuilder(); + clearSearch.and("msid", clearSearch.entity().getMsid(), SearchCriteria.Op.EQ); + } + + @DB + public void pushCleanupDelegate(long msid, int seq, String delegateClzName, Object context) { + StackMaidVO delegateItem = new StackMaidVO(); + delegateItem.setMsid(msid); + delegateItem.setThreadId(Thread.currentThread().getId()); + delegateItem.setSeq(seq); + delegateItem.setDelegate(delegateClzName); + delegateItem.setContext(SerializerHelper.toSerializedString(context)); + delegateItem.setCreated(DateUtil.currentGMTTime()); + + super.persist(delegateItem); + } + + @DB + public StackMaidVO popCleanupDelegate(long msid) { + SearchCriteria sc = popSearch.create(); + sc.setParameters("msid", msid); + sc.setParameters("threadId", Thread.currentThread().getId()); + + Filter filter = new Filter(StackMaidVO.class, "seq", false, 0L, (long)1); + List l = listBy(sc, filter); + if(l != null && l.size() > 0) { + delete(l.get(0).getId()); + return l.get(0); + } + + return null; + } + + @DB + public void clearStack(long msid) { + SearchCriteria sc = clearSearch.create(); + sc.setParameters("msid", msid); + + delete(sc); + } + + @DB + public List listLeftoversByMsid(long msid) { + List l = new ArrayList(); + String sql = "select * from stack_maid where msid=? order by msid asc, thread_id asc, seq desc"; + + Transaction txn = Transaction.currentTxn();; + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, msid); + + ResultSet rs = pstmt.executeQuery(); + while(rs.next()) { + l.add(toEntityBean(rs, false)); + } + } catch (SQLException e) { + s_logger.error("unexcpected exception " + e.getMessage(), e); + } catch (Throwable e) { + s_logger.error("unexcpected exception " + e.getMessage(), e); + } + return l; + } + + @DB + public List listLeftoversByCutTime(Date cutTime) { + + List l = new ArrayList(); + String sql = "select * from stack_maid where created < ? order by msid asc, thread_id asc, seq desc"; + + Transaction txn = Transaction.currentTxn();; + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(sql); + String gmtCutTime = DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), cutTime); + pstmt.setString(1, gmtCutTime); + + ResultSet rs = pstmt.executeQuery(); + while(rs.next()) { + l.add(toEntityBean(rs, false)); + } + } catch (SQLException e) { + s_logger.error("unexcpected exception " + e.getMessage(), e); + } catch (Throwable e) { + s_logger.error("unexcpected exception " + e.getMessage(), e); + } + return l; + } +} + diff --git a/core/src/com/cloud/network/BasicVirtualNetworkAllocator.java b/core/src/com/cloud/network/BasicVirtualNetworkAllocator.java new file mode 100644 index 00000000000..7a157114917 --- /dev/null +++ b/core/src/com/cloud/network/BasicVirtualNetworkAllocator.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.network; + +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.host.HostVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.user.AccountVO; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.vm.VMInstanceVO; + +@Local(value=VirtualNetworkAllocator.class) +public class BasicVirtualNetworkAllocator implements VirtualNetworkAllocator { + DataCenterDao _dcDao; + String _name; + + public BasicVirtualNetworkAllocator() { + } + + @Override + public String allocateTag(AccountVO account, HostVO host, VMInstanceVO vm, ServiceOfferingVO so) { + return _dcDao.allocateVnet(host.getDataCenterId(), account.getId()); + } + + @Override + public void releaseTag(String tag, HostVO host, AccountVO account, VMInstanceVO vm) { + _dcDao.releaseVnet(tag, host.getDataCenterId(), account.getId()); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + ComponentLocator locator = ComponentLocator.getCurrentLocator(); + _dcDao = locator.getDao(DataCenterDao.class); + if (_dcDao == null) { + throw new ConfigurationException("Unable to get DataCenterDao"); + } + _name = name; + return true; + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } + +} diff --git a/core/src/com/cloud/network/ExteralIpAddressAllocator.java b/core/src/com/cloud/network/ExteralIpAddressAllocator.java new file mode 100644 index 00000000000..b9506b34885 --- /dev/null +++ b/core/src/com/cloud/network/ExteralIpAddressAllocator.java @@ -0,0 +1,170 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.configuration.dao.ConfigurationDao; + +import com.cloud.dc.dao.VlanDao; +import com.cloud.network.dao.IPAddressDao; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.component.Inject; +import com.cloud.utils.exception.CloudRuntimeException; + +@Local(value=IpAddrAllocator.class) +public class ExteralIpAddressAllocator implements IpAddrAllocator{ + private static final Logger s_logger = Logger.getLogger(ExteralIpAddressAllocator.class); + String _name; + @Inject ConfigurationDao _configDao = null; + @Inject IPAddressDao _ipAddressDao = null; + @Inject VlanDao _vlanDao; + private boolean _isExternalIpAllocatorEnabled = false; + private String _externalIpAllocatorUrl = null; + + + @Override + public IpAddr getPrivateIpAddress(String macAddr, long dcId, long podId) { + if (_externalIpAllocatorUrl == null || this._externalIpAllocatorUrl.equalsIgnoreCase("")) { + return new IpAddr(); + } + String urlString = this._externalIpAllocatorUrl + "?command=getIpAddr&mac=" + macAddr + "&dc=" + dcId + "&pod=" + podId; + s_logger.debug("getIP:" + urlString); + + BufferedReader in = null; + try { + URL url = new URL(urlString); + URLConnection conn = url.openConnection(); + conn.setReadTimeout(30000); + + in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String inputLine; + while ((inputLine = in.readLine()) != null) { + s_logger.debug(inputLine); + String[] tokens = inputLine.split(","); + if (tokens.length != 3) { + s_logger.debug("the return value should be: mac,netmask,gateway"); + return new IpAddr(); + } + return new IpAddr(tokens[0], tokens[1], tokens[2]); + } + + return new IpAddr(); + } catch (MalformedURLException e) { + throw new CloudRuntimeException("URL is malformed " + urlString, e); + } catch (IOException e) { + return new IpAddr(); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + } + } + } + + } + + @Override + public IpAddr getPublicIpAddress(String macAddr, long dcId, long podId) { + /*TODO: call API to get ip address from external DHCP server*/ + return getPrivateIpAddress(macAddr, dcId, podId); + } + + @Override + public boolean releasePrivateIpAddress(String ip, long dcId, long podId) { + /*TODO: call API to release the ip address from external DHCP server*/ + if (_externalIpAllocatorUrl == null || this._externalIpAllocatorUrl.equalsIgnoreCase("")) { + return false; + } + + String urlString = this._externalIpAllocatorUrl + "?command=releaseIpAddr&ip=" + ip + "&dc=" + dcId + "&pod=" + podId; + + s_logger.debug("releaseIP:" + urlString); + BufferedReader in = null; + try { + URL url = new URL(urlString); + URLConnection conn = url.openConnection(); + conn.setReadTimeout(30000); + + in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + + return true; + } catch (MalformedURLException e) { + throw new CloudRuntimeException("URL is malformed " + urlString, e); + } catch (IOException e) { + return false; + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public boolean releasePublicIpAddress(String ip, long dcId, long podId) { + /*TODO: call API to release the ip address from external DHCP server*/ + return releasePrivateIpAddress(ip, dcId, podId); + } + + public boolean exteralIpAddressAllocatorEnabled() { + return _isExternalIpAllocatorEnabled; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + ComponentLocator locator = ComponentLocator.getCurrentLocator(); + _configDao = locator.getDao(ConfigurationDao.class); + _isExternalIpAllocatorEnabled = Boolean.parseBoolean(_configDao.getValue("direct.attach.network.externalIpAllocator.enabled")); + _externalIpAllocatorUrl = _configDao.getValue("direct.attach.network.externalIpAllocator.url"); + _name = name; + + return true; + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } +} diff --git a/core/src/com/cloud/network/FirewallRuleVO.java b/core/src/com/cloud/network/FirewallRuleVO.java new file mode 100644 index 00000000000..86225d1cfac --- /dev/null +++ b/core/src/com/cloud/network/FirewallRuleVO.java @@ -0,0 +1,179 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Transient; + +import com.google.gson.Gson; + +/** + * A bean representing a IP Forwarding + * + * @author Will Chan + * + */ +@Entity +@Table(name=("ip_forwarding")) +public class FirewallRuleVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="group_id") + private Long groupId; + + @Column(name="public_ip_address") + private String publicIpAddress = null; + + @Column(name="public_port") + private String publicPort = null; + + @Column(name="private_ip_address") + private String privateIpAddress = null; + + @Column(name="private_port") + private String privatePort = null; + + @Column(name="enabled") + private boolean enabled = false; + + @Column(name="protocol") + private String protocol = "TCP"; + + @Column(name="forwarding") + private boolean forwarding = true; + + @Column(name="algorithm") + private String algorithm = null; + + @Transient + private String vlanNetmask; + + public FirewallRuleVO() { + } + + public FirewallRuleVO(Long id, Long groupId, String publicIpAddress, String publicPort, String privateIpAddress, String privatePort, boolean enabled, String protocol, + boolean forwarding, String algorithm) { + this.id = id; + this.groupId = groupId; + this.publicIpAddress = publicIpAddress; + this.publicPort = publicPort; + this.privateIpAddress = privateIpAddress; + this.privatePort = privatePort; + this.enabled = enabled; + this.protocol = protocol; + } + + public FirewallRuleVO(FirewallRuleVO fwRule) { + this(fwRule.getId(), fwRule.getGroupId(), fwRule.getPublicIpAddress(), + fwRule.getPublicPort(), fwRule.getPrivateIpAddress(), + fwRule.getPrivatePort(), fwRule.isEnabled(), fwRule.getProtocol(), + fwRule.isForwarding(), fwRule.getAlgorithm()); + } + + public Long getId() { + return id; + } + + public Long getGroupId() { + return groupId; + } + + public void setGroupId(Long groupId) { + this.groupId = groupId; + } + + public String getPublicIpAddress() { + return publicIpAddress; + } + + public void setPublicIpAddress(String address) { + this.publicIpAddress = address; + } + + public String getPublicPort() { + return publicPort; + } + + public void setPublicPort(String port) { + this.publicPort = port; + } + + public String getPrivateIpAddress() { + return privateIpAddress; + } + + public void setPrivateIpAddress(String privateIpAddress) { + this.privateIpAddress = privateIpAddress; + } + + public String getPrivatePort() { + return privatePort; + } + + public void setPrivatePort(String privatePort) { + this.privatePort = privatePort; + } + public boolean isEnabled() { + return enabled; + } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public String getProtocol() { + return this.protocol; + } + public void setProtocol(String protocol) { + this.protocol = protocol; + } + public boolean isForwarding() { + return forwarding; + } + public void setForwarding(boolean forwarding) { + this.forwarding = forwarding; + } + public String getAlgorithm() { + return algorithm; + } + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public String toString() { + Gson gson = new Gson(); + return gson.toJson(this); + } + + public void setVlanNetmask(String vlanNetmask) { + this.vlanNetmask = vlanNetmask; + } + + public String getVlanNetmask() { + return vlanNetmask; + } + +} + diff --git a/core/src/com/cloud/network/HAProxyConfigurator.java b/core/src/com/cloud/network/HAProxyConfigurator.java new file mode 100644 index 00000000000..fbd8476d7d4 --- /dev/null +++ b/core/src/com/cloud/network/HAProxyConfigurator.java @@ -0,0 +1,183 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.network; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * @author chiradeep + * + */ +public class HAProxyConfigurator implements LoadBalancerConfigurator { + private static String [] globalSection = {"global", + "\tlog 127.0.0.1:3914 local0 info", + "\tmaxconn 4096", + "\tchroot /var/lib/haproxy", + "\tuser haproxy", + "\tgroup haproxy", + "\tdaemon" + }; + + private static String [] defaultsSection = {"defaults", + "\tlog global", + "\tmode tcp", + "\toption dontlognull", + "\tretries 3", + "\toption redispatch", + "\toption forwardfor", + "\tstats enable", + "\tstats uri /admin?stats", + "\tstats realm Haproxy\\ Statistics", + "\tstats auth admin1:AdMiN123", + "\toption forceclose", + "\ttimeout connect 5000", + "\ttimeout client 50000", + "\ttimeout server 50000" + }; + + private static String [] defaultListen = {"listen vmops 0.0.0.0:9", + "\toption transparent" + }; + + @Override + public String[] generateConfiguration(List fwRules) { + //Group the rules by publicip:publicport + Map> pools = new HashMap>(); + + for(FirewallRuleVO rule:fwRules) { + StringBuilder sb = new StringBuilder(); + String poolName = sb.append(rule.getPublicIpAddress().replace(".", "_")).append('-').append(rule.getPublicPort()).toString(); + if (rule.isEnabled() && !rule.isForwarding()) { + List fwList = pools.get(poolName); + if (fwList == null) { + fwList = new ArrayList(); + pools.put(poolName, fwList); + } + fwList.add(rule); + } + } + + List result = new ArrayList(); + + result.addAll(Arrays.asList(globalSection)); + result.add(getBlankLine()); + result.addAll(Arrays.asList(defaultsSection)); + result.add(getBlankLine()); + + if (pools.isEmpty()){ + //haproxy cannot handle empty listen / frontend or backend, so add a dummy listener + //on port 9 + result.addAll(Arrays.asList(defaultListen)); + } + result.add(getBlankLine()); + + for (Map.Entry> e : pools.entrySet()){ + List poolRules = getRulesForPool(e.getKey(), e.getValue()); + result.addAll(poolRules); + } + + return result.toArray(new String[result.size()]); + } + + private List getRulesForPool(String poolName, List fwRules) { + FirewallRuleVO firstRule = fwRules.get(0); + String publicIP = firstRule.getPublicIpAddress(); + String publicPort = firstRule.getPublicPort(); + String algorithm = firstRule.getAlgorithm(); + + List result = new ArrayList(); + //add line like this: "listen 65_37_141_30-80 65.37.141.30:80" + StringBuilder sb = new StringBuilder(); + sb.append("listen ").append(poolName).append(" ") + .append(publicIP).append(":").append(publicPort); + result.add(sb.toString()); + sb = new StringBuilder(); + sb.append("\t").append("balance ").append(algorithm); + result.add(sb.toString()); + int i=0; + for (FirewallRuleVO rule: fwRules) { + //add line like this: "server 65_37_141_30-80_3 10.1.1.4:80 check" + if (!rule.isEnabled()) + continue; + sb = new StringBuilder(); + sb.append("\t").append("server ").append(poolName) + .append("_").append(Integer.toString(i++)).append(" ") + .append(rule.getPrivateIpAddress()).append(":").append(rule.getPrivatePort()) + .append(" check"); + result.add(sb.toString()); + } + result.add(getBlankLine()); + return result; + } + + private String getBlankLine() { + return new String("\t "); + } + + public static void main(String [] args) { +/* FirewallRulesDao dao = new FirewallRulesDaoImpl(); + List rules = dao.listIPForwarding(); + + HAProxyConfigurator hapc = new HAProxyConfigurator(); + String [] result = hapc.generateConfiguration(rules); + for (int i=0; i < result.length; i++) { + System.out.println(result[i]); + }*/ + + } + + @Override + public String[][] generateFwRules(List fwRules) { + String [][] result = new String [2][]; + Set toAdd = new HashSet(); + Set toRemove = new HashSet(); + + for (int i = 0; i < fwRules.size(); i++) { + FirewallRuleVO rule = fwRules.get(i); + if (rule.isForwarding()) + continue; + + String vlanNetmask = rule.getVlanNetmask(); + + StringBuilder sb = new StringBuilder(); + sb.append(rule.getPublicIpAddress()).append(":"); + sb.append(rule.getPublicPort()).append(":"); + sb.append(vlanNetmask); + String lbRuleEntry = sb.toString(); + if (rule.isEnabled()) { + toAdd.add(lbRuleEntry); + } else { + toRemove.add(lbRuleEntry); + } + } + toRemove.removeAll(toAdd); + result[ADD] = toAdd.toArray(new String[toAdd.size()]); + result[REMOVE] = toRemove.toArray(new String[toRemove.size()]); + + return result; + } + +} diff --git a/core/src/com/cloud/network/IPAddressVO.java b/core/src/com/cloud/network/IPAddressVO.java new file mode 100644 index 00000000000..39527f6e5b8 --- /dev/null +++ b/core/src/com/cloud/network/IPAddressVO.java @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +/** + * A bean representing a public IP Address + * + * @author Will Chan + * + */ +@Entity +@Table(name=("user_ip_address")) +public class IPAddressVO { + @Column(name="account_id") + private Long accountId = null; + + @Column(name="domain_id") + private Long domainId = null; + + @Id + @Column(name="public_ip_address") + private String address = null; + + @Column(name="data_center_id", updatable=false) + private long dataCenterId; + + @Column(name="source_nat") + private boolean sourceNat; + + @Column(name="allocated") + @Temporal(value=TemporalType.TIMESTAMP) + private Date allocated; + + @Column(name="vlan_db_id") + private long vlanDbId; + + protected IPAddressVO() { + } + + public IPAddressVO(String address, long dataCenterId, long vlanDbId, boolean sourceNat) { + this.address = address; + this.dataCenterId = dataCenterId; + this.vlanDbId = vlanDbId; + this.sourceNat = sourceNat; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public String getAddress() { + return address; + } + public Long getAccountId() { + return accountId; + } + public Long getDomainId() { + return domainId; + } + public Date getAllocated() { + return allocated; + } + public boolean isSourceNat() { + return sourceNat; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public void setSourceNat(boolean sourceNat) { + this.sourceNat = sourceNat; + } + + public boolean getSourceNat() { + return this.sourceNat; + } + + public void setAllocated(Date allocated) { + this.allocated = allocated; + } + + public long getVlanDbId() { + return this.vlanDbId; + } + + public void setVlanDbId(long vlanDbId) { + this.vlanDbId = vlanDbId; + } + +} diff --git a/core/src/com/cloud/network/IpAddrAllocator.java b/core/src/com/cloud/network/IpAddrAllocator.java new file mode 100644 index 00000000000..2d3047bb280 --- /dev/null +++ b/core/src/com/cloud/network/IpAddrAllocator.java @@ -0,0 +1,40 @@ +package com.cloud.network; + +import com.cloud.utils.component.Adapter; + +public interface IpAddrAllocator extends Adapter { + public class IpAddr { + public String ipaddr; + public String netMask; + public String gateway; + public IpAddr(String ipaddr, String netMask, String gateway) { + this.ipaddr = ipaddr; + this.netMask = netMask; + this.gateway = gateway; + } + public IpAddr() { + this.ipaddr = null; + this.netMask = null; + this.gateway = null; + } + } + public class networkInfo { + public String _ipAddr; + public String _netMask; + public String _gateWay; + public Long _vlanDbId; + public String _vlanid; + public networkInfo(String ip, String netMask, String gateway, Long vlanDbId, String vlanId) { + _ipAddr = ip; + _netMask = netMask; + _gateWay = gateway; + _vlanDbId = vlanDbId; + _vlanid = vlanId; + } + } + public IpAddr getPublicIpAddress(String macAddr, long dcId, long podId); + public IpAddr getPrivateIpAddress(String macAddr, long dcId, long podId); + public boolean releasePublicIpAddress(String ip, long dcId, long podId); + public boolean releasePrivateIpAddress(String ip, long dcId, long podId); + public boolean exteralIpAddressAllocatorEnabled(); +} diff --git a/core/src/com/cloud/network/LoadBalancerConfigurator.java b/core/src/com/cloud/network/LoadBalancerConfigurator.java new file mode 100644 index 00000000000..df7bda4a658 --- /dev/null +++ b/core/src/com/cloud/network/LoadBalancerConfigurator.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.network; + +import java.util.List; + + +/** + * @author chiradeep + * + */ +public interface LoadBalancerConfigurator { + public final static int ADD = 0; + public final static int REMOVE = 1; + + public String [] generateConfiguration(List fwRules); + public String [][] generateFwRules(List fwRules); +} diff --git a/core/src/com/cloud/network/LoadBalancerVMMapVO.java b/core/src/com/cloud/network/LoadBalancerVMMapVO.java new file mode 100644 index 00000000000..72d8599bc25 --- /dev/null +++ b/core/src/com/cloud/network/LoadBalancerVMMapVO.java @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name=("load_balancer_vm_map")) +public class LoadBalancerVMMapVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="load_balancer_id") + private long loadBalancerId; + + @Column(name="instance_id") + private long instanceId; + + @Column(name="pending") + private boolean pending = false; + + public LoadBalancerVMMapVO() { } + + public LoadBalancerVMMapVO(long loadBalancerId, long instanceId) { + this.loadBalancerId = loadBalancerId; + this.instanceId = instanceId; + } + + public LoadBalancerVMMapVO(long loadBalancerId, long instanceId, boolean pending) { + this.loadBalancerId = loadBalancerId; + this.instanceId = instanceId; + this.pending = pending; + } + + public Long getId() { + return id; + } + + public long getLoadBalancerId() { + return loadBalancerId; + } + + public long getInstanceId() { + return instanceId; + } + + public boolean isPending() { + return pending; + } + + public void setPending(boolean pending) { + this.pending = pending; + } +} diff --git a/core/src/com/cloud/network/LoadBalancerVO.java b/core/src/com/cloud/network/LoadBalancerVO.java new file mode 100644 index 00000000000..11b1fc7aa72 --- /dev/null +++ b/core/src/com/cloud/network/LoadBalancerVO.java @@ -0,0 +1,130 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.SecondaryTable; +import javax.persistence.Table; + +@Entity +@Table(name=("load_balancer")) +@SecondaryTable(name="account", + pkJoinColumns={@PrimaryKeyJoinColumn(name="account_id", referencedColumnName="id")}) +public class LoadBalancerVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="name") + private String name; + + @Column(name="description") + private String description; + + @Column(name="account_id") + private long accountId; + + @Column(name="domain_id", table="account", insertable=false, updatable=false) + private long domainId; + + @Column(name="account_name", table="account", insertable=false, updatable=false) + private String accountName = null; + + @Column(name="ip_address") + private String ipAddress; + + @Column(name="public_port") + private String publicPort; + + @Column(name="private_port") + private String privatePort; + + @Column(name="algorithm") + private String algorithm; + + public LoadBalancerVO() { } + + public LoadBalancerVO(String name, String description, long accountId, String ipAddress, String publicPort, String privatePort, String algorithm) { + this.name = name; + this.description = description; + this.accountId = accountId; + this.ipAddress = ipAddress; + this.publicPort = publicPort; + this.privatePort = privatePort; + this.algorithm = algorithm; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + + public long getAccountId() { + return accountId; + } + + public String getIpAddress() { + return ipAddress; + } + + public String getPublicPort() { + return publicPort; + } + + public String getPrivatePort() { + return privatePort; + } + public void setPrivatePort(String privatePort) { + this.privatePort = privatePort; + } + + public String getAlgorithm() { + return algorithm; + } + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public Long getDomainId() { + return domainId; + } + + public String getAccountName() { + return accountName; + } +} diff --git a/core/src/com/cloud/network/NetworkEnums.java b/core/src/com/cloud/network/NetworkEnums.java new file mode 100644 index 00000000000..100507d30f8 --- /dev/null +++ b/core/src/com/cloud/network/NetworkEnums.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.network; + +/** + * @author chiradeep + * + */ +public class NetworkEnums { + public enum RouterPrivateIpStrategy { + None, + DcGlobal, //global to data center + HostLocal; + + public static String DummyPrivateIp = "169.254.1.1"; + } +} diff --git a/core/src/com/cloud/network/NetworkRuleConfigVO.java b/core/src/com/cloud/network/NetworkRuleConfigVO.java new file mode 100644 index 00000000000..b013ca5a1d9 --- /dev/null +++ b/core/src/com/cloud/network/NetworkRuleConfigVO.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.async.AsyncInstanceCreateStatus; +import com.google.gson.annotations.Expose; + +@Entity +@Table(name=("network_rule_config")) +public class NetworkRuleConfigVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="security_group_id") + private long securityGroupId; + + @Column(name="public_port") + private String publicPort; + + @Column(name="private_port") + private String privatePort; + + @Column(name="protocol") + private String protocol; + + @Expose + @Column(name="create_status", updatable = true, nullable=false) + @Enumerated(value=EnumType.STRING) + private AsyncInstanceCreateStatus createStatus; + + public NetworkRuleConfigVO() {} + + public NetworkRuleConfigVO(long securityGroupId, String publicPort, String privatePort, String protocol) { + this.securityGroupId = securityGroupId; + this.publicPort = publicPort; + this.privatePort = privatePort; + this.protocol = protocol; + } + + public Long getId() { + return id; + } + + public long getSecurityGroupId() { + return securityGroupId; + } + + public String getPublicPort() { + return publicPort; + } + + public String getPrivatePort() { + return privatePort; + } + + public String getProtocol() { + return protocol; + } + + public AsyncInstanceCreateStatus getCreateStatus() { + return createStatus; + } + + public void setCreateStatus(AsyncInstanceCreateStatus createStatus) { + this.createStatus = createStatus; + } +} diff --git a/core/src/com/cloud/network/SecurityGroupVMMapVO.java b/core/src/com/cloud/network/SecurityGroupVMMapVO.java new file mode 100644 index 00000000000..590b744a387 --- /dev/null +++ b/core/src/com/cloud/network/SecurityGroupVMMapVO.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name=("security_group_vm_map")) +public class SecurityGroupVMMapVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="security_group_id") + private long securityGroupId; + + @Column(name="ip_address") + private String ipAddress; + + @Column(name="instance_id") + private long instanceId; + + public SecurityGroupVMMapVO() { } + + public SecurityGroupVMMapVO(long securityGroupId, String ipAddress, long instanceId) { + this.securityGroupId = securityGroupId; + this.ipAddress = ipAddress; + this.instanceId = instanceId; + } + + public Long getId() { + return id; + } + + public long getSecurityGroupId() { + return securityGroupId; + } + + public String getIpAddress() { + return ipAddress; + } + + public long getInstanceId() { + return instanceId; + } +} diff --git a/core/src/com/cloud/network/SecurityGroupVO.java b/core/src/com/cloud/network/SecurityGroupVO.java new file mode 100644 index 00000000000..8ea991e01a9 --- /dev/null +++ b/core/src/com/cloud/network/SecurityGroupVO.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.SecondaryTable; +import javax.persistence.Table; + +@Entity +@Table(name=("security_group")) +@SecondaryTable(name="account", + pkJoinColumns={@PrimaryKeyJoinColumn(name="account_id", referencedColumnName="id")}) +public class SecurityGroupVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="name") + private String name; + + @Column(name="description") + private String description; + + @Column(name="domain_id") + private Long domainId; + + @Column(name="account_id") + private Long accountId; + + @Column(name="account_name", table="account", insertable=false, updatable=false) + private String accountName = null; + + public SecurityGroupVO() {} + + public SecurityGroupVO(String name, String description, Long domainId, Long accountId) { + this.name = name; + this.description = description; + this.domainId = domainId; + this.accountId = accountId; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Long getDomainId() { + return domainId; + } + + public Long getAccountId() { + return accountId; + } + + public String getAccountName() { + return accountName; + } +} diff --git a/core/src/com/cloud/network/VirtualNetworkAllocator.java b/core/src/com/cloud/network/VirtualNetworkAllocator.java new file mode 100644 index 00000000000..c57bccf5dcb --- /dev/null +++ b/core/src/com/cloud/network/VirtualNetworkAllocator.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.network; + +import com.cloud.host.HostVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.user.AccountVO; +import com.cloud.utils.component.Adapter; +import com.cloud.vm.VMInstanceVO; + +public interface VirtualNetworkAllocator extends Adapter { + + /** + * Allocate an virtual network tag + * + * @param account account that this network belongs to. + * @param host host being deployed to. + * @param dc datacenter being deployed to. + * @param vm vm that is being deployed. + * @param so service offering for that vm. + * @return tag + */ + String allocateTag(AccountVO account, HostVO host, VMInstanceVO vm, ServiceOfferingVO so); + + + /** + * Release the virtual network tag. + * + * @param tag tag retrieved in allocateTag + * @param host host to release this tag in. + * @param account account to release this tag in. + * @param vm vm that is releasing this tag. + */ + void releaseTag(String tag, HostVO host, AccountVO account, VMInstanceVO vm); + +} diff --git a/core/src/com/cloud/network/dao/FirewallRulesDao.java b/core/src/com/cloud/network/dao/FirewallRulesDao.java new file mode 100644 index 00000000000..7db42df3bf6 --- /dev/null +++ b/core/src/com/cloud/network/dao/FirewallRulesDao.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.List; + +import com.cloud.network.FirewallRuleVO; +import com.cloud.utils.db.GenericDao; + +/* + * Data Access Object for user_ip_address and ip_forwarding tables + */ +public interface FirewallRulesDao extends GenericDao { + public List listIPForwarding(String publicIPAddress, boolean forwarding); + public List listIPForwarding(String publicIPAddress, String port, boolean forwarding); + + public List listIPForwarding(long userId); + public List listIPForwarding(long userId, long dcId); + public void deleteIPForwardingByPublicIpAddress(String ipAddress); + public List listIPForwarding(String publicIPAddress); + public List listIPForwardingForUpdate(String publicIPAddress); + public void disableIPForwarding(String publicIPAddress); + public List listIPForwardingForUpdate(String publicIp, boolean fwding); + public List listIPForwardingForUpdate(String publicIp, String publicPort, String proto); + public List listLoadBalanceRulesForUpdate(String publicIp, String publicPort, String algo); + + public List listRulesExcludingPubIpPort(String publicIpAddress, long securityGroupId); + public List listBySecurityGroupId(long securityGroupId); + public List listByLoadBalancerId(long loadBalancerId); + public List listForwardingByPubAndPrivIp(boolean forwarding, String publicIPAddress, String privateIp); + public FirewallRuleVO findByGroupAndPrivateIp(long groupId, String privateIp, boolean forwarding); +} diff --git a/core/src/com/cloud/network/dao/FirewallRulesDaoImpl.java b/core/src/com/cloud/network/dao/FirewallRulesDaoImpl.java new file mode 100644 index 00000000000..e714f351d22 --- /dev/null +++ b/core/src/com/cloud/network/dao/FirewallRulesDaoImpl.java @@ -0,0 +1,310 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.network.FirewallRuleVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.exception.CloudRuntimeException; + +@Local(value = { FirewallRulesDao.class }) +public class FirewallRulesDaoImpl extends GenericDaoBase implements FirewallRulesDao { + private static final Logger s_logger = Logger.getLogger(FirewallRulesDaoImpl.class); + + public static String SELECT_IP_FORWARDINGS_BY_USERID_SQL = null; + public static String SELECT_IP_FORWARDINGS_BY_USERID_AND_DCID_SQL = null; + + public static final String DELETE_IP_FORWARDING_BY_IPADDRESS_SQL = "DELETE FROM ip_forwarding WHERE public_ip_address = ?"; + public static final String DISABLE_IP_FORWARDING_BY_IPADDRESS_SQL = "UPDATE ip_forwarding set enabled=0 WHERE public_ip_address = ?"; + + + protected SearchBuilder FWByIPSearch; + protected SearchBuilder FWByIPAndForwardingSearch; + protected SearchBuilder FWByIPPortAndForwardingSearch; + protected SearchBuilder FWByIPPortProtoSearch; + protected SearchBuilder FWByIPPortAlgoSearch; + protected SearchBuilder FWByPrivateIPSearch; + protected SearchBuilder RulesExcludingPubIpPort; + protected SearchBuilder FWByGroupId; + protected SearchBuilder FWByGroupAndPrivateIp; + + protected FirewallRulesDaoImpl() { + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + if (!super.configure(name, params)) { + return false; + } + + SELECT_IP_FORWARDINGS_BY_USERID_SQL = buildSelectByUserIdSql(); + if (s_logger.isDebugEnabled()) { + s_logger.debug(SELECT_IP_FORWARDINGS_BY_USERID_SQL); + } + + SELECT_IP_FORWARDINGS_BY_USERID_AND_DCID_SQL = buildSelectByUserIdAndDatacenterIdSql(); + if (s_logger.isDebugEnabled()) { + s_logger.debug(SELECT_IP_FORWARDINGS_BY_USERID_AND_DCID_SQL); + } + + FWByIPSearch = createSearchBuilder(); + FWByIPSearch.and("publicIpAddress", FWByIPSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); + FWByIPSearch.done(); + + FWByIPAndForwardingSearch = createSearchBuilder(); + FWByIPAndForwardingSearch.and("publicIpAddress", FWByIPAndForwardingSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); + FWByIPAndForwardingSearch.and("forwarding", FWByIPAndForwardingSearch.entity().isForwarding(), SearchCriteria.Op.EQ); + FWByIPAndForwardingSearch.done(); + + FWByIPPortAndForwardingSearch = createSearchBuilder(); + FWByIPPortAndForwardingSearch.and("publicIpAddress", FWByIPPortAndForwardingSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); + FWByIPPortAndForwardingSearch.and("publicPort", FWByIPPortAndForwardingSearch.entity().getPublicPort(), SearchCriteria.Op.EQ); + FWByIPPortAndForwardingSearch.and("forwarding", FWByIPPortAndForwardingSearch.entity().isForwarding(), SearchCriteria.Op.EQ); + FWByIPPortAndForwardingSearch.done(); + + FWByIPPortProtoSearch = createSearchBuilder(); + FWByIPPortProtoSearch.and("publicIpAddress", FWByIPPortProtoSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); + FWByIPPortProtoSearch.and("publicPort", FWByIPPortProtoSearch.entity().getPublicPort(), SearchCriteria.Op.EQ); + FWByIPPortProtoSearch.and("protocol", FWByIPPortProtoSearch.entity().getProtocol(), SearchCriteria.Op.EQ); + FWByIPPortProtoSearch.done(); + + FWByIPPortAlgoSearch = createSearchBuilder(); + FWByIPPortAlgoSearch.and("publicIpAddress", FWByIPPortAlgoSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); + FWByIPPortAlgoSearch.and("publicPort", FWByIPPortAlgoSearch.entity().getPublicPort(), SearchCriteria.Op.EQ); + FWByIPPortAlgoSearch.and("algorithm", FWByIPPortAlgoSearch.entity().getAlgorithm(), SearchCriteria.Op.EQ); + FWByIPPortAlgoSearch.done(); + + FWByPrivateIPSearch = createSearchBuilder(); + FWByPrivateIPSearch.and("privateIpAddress", FWByPrivateIPSearch.entity().getPrivateIpAddress(), SearchCriteria.Op.EQ); + FWByPrivateIPSearch.done(); + + RulesExcludingPubIpPort = createSearchBuilder(); + RulesExcludingPubIpPort.and("publicIpAddress", RulesExcludingPubIpPort.entity().getPrivateIpAddress(), SearchCriteria.Op.EQ); + RulesExcludingPubIpPort.and("groupId", RulesExcludingPubIpPort.entity().getGroupId(), SearchCriteria.Op.NEQ); + RulesExcludingPubIpPort.and("forwarding", RulesExcludingPubIpPort.entity().isForwarding(), SearchCriteria.Op.EQ); + RulesExcludingPubIpPort.done(); + + FWByGroupId = createSearchBuilder(); + FWByGroupId.and("groupId", FWByGroupId.entity().getGroupId(), SearchCriteria.Op.EQ); + FWByGroupId.and("forwarding", FWByGroupId.entity().isForwarding(), SearchCriteria.Op.EQ); + FWByGroupId.done(); + + FWByGroupAndPrivateIp = createSearchBuilder(); + FWByGroupAndPrivateIp.and("groupId", FWByGroupAndPrivateIp.entity().getGroupId(), SearchCriteria.Op.EQ); + FWByGroupAndPrivateIp.and("privateIpAddress", FWByGroupAndPrivateIp.entity().getPrivateIpAddress(), SearchCriteria.Op.EQ); + FWByGroupAndPrivateIp.and("forwarding", FWByGroupAndPrivateIp.entity().isForwarding(), SearchCriteria.Op.EQ); + FWByGroupAndPrivateIp.done(); + + return true; + } + + protected String buildSelectByUserIdSql() { + StringBuilder sql = createPartialSelectSql(null, true); + sql.insert(sql.length() - 6, ", user_ip_address "); + sql.append("ip_forwarding.public_ip_address = user_ip_address.public_ip_address AND user_ip_address.account_id = ?"); + + return sql.toString(); + } + + protected String buildSelectByUserIdAndDatacenterIdSql() { + return "SELECT i.id, i.group_id, i.public_ip_address, i.public_port, i.private_ip_address, i.private_port, i.enabled, i.protocol, i.forwarding, i.algorithm FROM ip_forwarding i, user_ip_address u WHERE i.public_ip_address=u.public_ip_address AND u.account_id=? AND u.data_center_id=?"; + } + + public List listIPForwarding(String publicIPAddress, boolean forwarding) { + SearchCriteria sc = FWByIPAndForwardingSearch.create(); + sc.setParameters("publicIpAddress", publicIPAddress); + sc.setParameters("forwarding", forwarding); + return listActiveBy(sc); + } + + @Override + public List listIPForwarding(long userId) { + Transaction txn = Transaction.currentTxn(); + List forwardings = new ArrayList(); + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(SELECT_IP_FORWARDINGS_BY_USERID_SQL); + pstmt.setLong(1, userId); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + forwardings.add(toEntityBean(rs, false)); + } + } catch (Exception e) { + s_logger.warn(e); + } + return forwardings; + } + + public List listIPForwarding(long userId, long dcId) { + Transaction txn = Transaction.currentTxn(); + List forwardings = new ArrayList(); + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(SELECT_IP_FORWARDINGS_BY_USERID_AND_DCID_SQL); + pstmt.setLong(1, userId); + pstmt.setLong(2, dcId); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + forwardings.add(toEntityBean(rs, false)); + } + } catch (Exception e) { + s_logger.warn(e); + } + return forwardings; + } + + @Override + public void deleteIPForwardingByPublicIpAddress(String ipAddress) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(DELETE_IP_FORWARDING_BY_IPADDRESS_SQL); + pstmt.setString(1, ipAddress); + pstmt.executeUpdate(); + } catch (Exception e) { + s_logger.warn(e); + } + } + + @Override + public List listIPForwarding(String publicIPAddress) { + SearchCriteria sc = FWByIPSearch.create(); + sc.setParameters("publicIpAddress", publicIPAddress); + return listActiveBy(sc); + } + + @Override + public List listIPForwardingForUpdate(String publicIPAddress) { + SearchCriteria sc = FWByIPSearch.create(); + sc.setParameters("publicIpAddress", publicIPAddress); + return listActiveBy(sc, null); + } + + @Override + public List listIPForwardingForUpdate(String publicIp, boolean fwding) { + SearchCriteria sc = FWByIPAndForwardingSearch.create(); + sc.setParameters("publicIpAddress", publicIp); + sc.setParameters("forwarding", fwding); + return search(sc, null); + } + + @Override + public List listIPForwardingForUpdate(String publicIp, + String publicPort, String proto) { + SearchCriteria sc = FWByIPPortProtoSearch.create(); + sc.setParameters("publicIpAddress", publicIp); + sc.setParameters("publicPort", publicPort); + sc.setParameters("protocol", proto); + return search(sc, null); + } + + @Override + public List listLoadBalanceRulesForUpdate(String publicIp, + String publicPort, String algo) { + SearchCriteria sc = FWByIPPortAlgoSearch.create(); + sc.setParameters("publicIpAddress", publicIp); + sc.setParameters("publicPort", publicPort); + sc.setParameters("algorithm", algo); + return listActiveBy(sc, null); + } + + @Override + public List listIPForwarding(String publicIPAddress, + String port, boolean forwarding) { + SearchCriteria sc = FWByIPPortAndForwardingSearch.create(); + sc.setParameters("publicIpAddress", publicIPAddress); + sc.setParameters("publicPort", port); + sc.setParameters("forwarding", forwarding); + + return listActiveBy(sc); + } + + @Override + public void disableIPForwarding(String publicIPAddress) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + txn.start(); + pstmt = txn.prepareAutoCloseStatement(DISABLE_IP_FORWARDING_BY_IPADDRESS_SQL); + pstmt.setString(1, publicIPAddress); + pstmt.executeUpdate(); + txn.commit(); + } catch (Exception e) { + txn.rollback(); + throw new CloudRuntimeException("DB Exception ", e); + } + } + + @Override + public List listRulesExcludingPubIpPort(String publicIpAddress, long securityGroupId) { + SearchCriteria sc = RulesExcludingPubIpPort.create(); + sc.setParameters("publicIpAddress", publicIpAddress); + sc.setParameters("groupId", securityGroupId); + sc.setParameters("forwarding", false); + return listActiveBy(sc); + } + + @Override + public List listBySecurityGroupId(long securityGroupId) { + SearchCriteria sc = FWByGroupId.create(); + sc.setParameters("groupId", securityGroupId); + sc.setParameters("forwarding", Boolean.TRUE); + return listActiveBy(sc); + } + + @Override + public List listForwardingByPubAndPrivIp(boolean forwarding, String publicIPAddress, String privateIp) { + SearchCriteria sc = FWByIPAndForwardingSearch.create(); + sc.setParameters("publicIpAddress", publicIPAddress); + sc.setParameters("forwarding", forwarding); + sc.addAnd("privateIpAddress", SearchCriteria.Op.EQ, privateIp); + return listActiveBy(sc); + } + + @Override + public List listByLoadBalancerId(long loadBalancerId) { + SearchCriteria sc = FWByGroupId.create(); + sc.setParameters("groupId", loadBalancerId); + sc.setParameters("forwarding", Boolean.FALSE); + return listActiveBy(sc); + } + + @Override + public FirewallRuleVO findByGroupAndPrivateIp(long groupId, String privateIp, boolean forwarding) { + SearchCriteria sc = FWByGroupAndPrivateIp.create(); + sc.setParameters("groupId", groupId); + sc.setParameters("privateIpAddress", privateIp); + sc.setParameters("forwarding", forwarding); + return findOneActiveBy(sc); + + } +} diff --git a/core/src/com/cloud/network/dao/IPAddressDao.java b/core/src/com/cloud/network/dao/IPAddressDao.java new file mode 100644 index 00000000000..45ec216f20b --- /dev/null +++ b/core/src/com/cloud/network/dao/IPAddressDao.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.List; + +import com.cloud.network.IPAddressVO; +import com.cloud.utils.db.GenericDao; + +public interface IPAddressDao extends GenericDao { + + /** + * @param accountId account id + * @param domainId id of the account's domain + * @param dcId data center id + * @param sourceNat is it for source nat? + * @return public ip address + */ + public String assignIpAddress(long accountId, long domainId, long vlanDbId, boolean sourceNat); + + public void unassignIpAddress(String ipAddress); + + public List listByAccount(long accountId); + + public List listByDcIdIpAddress(long dcId, String ipAddress); + + public int countIPs(long dcId, long vlanDbId, boolean onlyCountAllocated); + + public int countIPs(long dcId, String vlanId, String vlanGateway, String vlanNetmask, boolean onlyCountAllocated); + + public boolean mark(long dcId, String ip); + + public List assignAcccountSpecificIps(long accountId, long longValue, Long vlanDbId, boolean sourceNat); + + public void setIpAsSourceNat(String ipAddr); + + void unassignIpAsSourceNat(String ipAddress); +} diff --git a/core/src/com/cloud/network/dao/IPAddressDaoImpl.java b/core/src/com/cloud/network/dao/IPAddressDaoImpl.java new file mode 100644 index 00000000000..cbb58710833 --- /dev/null +++ b/core/src/com/cloud/network/dao/IPAddressDaoImpl.java @@ -0,0 +1,242 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.network.IPAddressVO; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value={IPAddressDao.class}) +public class IPAddressDaoImpl extends GenericDaoBase implements IPAddressDao { + private static final Logger s_logger = Logger.getLogger(IPAddressDaoImpl.class); + + protected SearchBuilder DcIpSearch; + protected SearchBuilder VlanDbIdSearchUnallocated; + protected SearchBuilder AccountSearch; + + // make it public for JUnit test + public IPAddressDaoImpl() { + DcIpSearch = createSearchBuilder(); + DcIpSearch.and("dataCenterId", DcIpSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DcIpSearch.and("ipAddress", DcIpSearch.entity().getAddress(), SearchCriteria.Op.EQ); + DcIpSearch.done(); + + VlanDbIdSearchUnallocated = createSearchBuilder(); + VlanDbIdSearchUnallocated.and("allocated", VlanDbIdSearchUnallocated.entity().getAllocated(), SearchCriteria.Op.NULL); + VlanDbIdSearchUnallocated.and("vlanDbId", VlanDbIdSearchUnallocated.entity().getVlanDbId(), SearchCriteria.Op.EQ); + //VlanDbIdSearchUnallocated.addRetrieve("ipAddress", VlanDbIdSearchUnallocated.entity().getAddress()); + VlanDbIdSearchUnallocated.done(); + + AccountSearch = createSearchBuilder(); + AccountSearch.and("accountId", AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountSearch.done(); + } + + public boolean mark(long dcId, String ip) { + SearchCriteria sc = DcIpSearch.create(); + sc.setParameters("dataCenterId", dcId); + sc.setParameters("ipAddress", ip); + + IPAddressVO vo = createForUpdate(); + vo.setAllocated(new Date()); + + return update(vo, sc) >= 1; + } + + @DB + public List assignAcccountSpecificIps(long accountId, long domainId, Long vlanDbId, boolean sourceNat) { + + SearchBuilder VlanDbIdSearch = createSearchBuilder(); + VlanDbIdSearch.and("vlanDbId", VlanDbIdSearch.entity().getVlanDbId(), SearchCriteria.Op.EQ); + VlanDbIdSearch.and("sourceNat", VlanDbIdSearch.entity().getSourceNat(), SearchCriteria.Op.EQ); + VlanDbIdSearch.done(); + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + SearchCriteria sc = VlanDbIdSearch.create(); + sc.setParameters("vlanDbId", vlanDbId); + sc.setParameters("sourceNat", sourceNat); + + List ipList = this.lock(sc, null, true); + List ipStringList = new ArrayList(); + + for(IPAddressVO ip:ipList){ + + ip.setAccountId(accountId); + ip.setAllocated(new Date()); + ip.setDomainId(domainId); + ip.setSourceNat(sourceNat); + + if (!update(ip.getAddress(), ip)) { + s_logger.debug("Unable to retrieve ip address " + ip.getAddress()); + return null; + } + ipStringList.add(ip.getAddress()); + } + txn.commit(); + return ipStringList; + } catch (Exception e) { + s_logger.warn("Unable to assign IP", e); + } + return null; + + } + public void setIpAsSourceNat(String ipAddr){ + + IPAddressVO ip = createForUpdate(ipAddr); + ip.setSourceNat(true); + s_logger.debug("Setting " + ipAddr + " as source Nat "); + update(ipAddr, ip); + } + + @Override + public String assignIpAddress(long accountId, long domainId, long vlanDbId, boolean sourceNat) { + + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + SearchCriteria sc = VlanDbIdSearchUnallocated.create(); + sc.setParameters("vlanDbId", vlanDbId); + + IPAddressVO ip = this.lock(sc, true); + if(ip != null) { + ip.setAccountId(accountId); + ip.setAllocated(new Date()); + ip.setDomainId(domainId); + ip.setSourceNat(sourceNat); + + if (!update(ip.getAddress(), ip)) { + s_logger.debug("Unable to retrieve any ip addresses"); + return null; + } + + txn.commit(); + return ip.getAddress(); + } else { + txn.rollback(); + s_logger.error("Unable to find an available IP address with related vlan, vlanDbId: " + vlanDbId); + } + } catch (Exception e) { + s_logger.warn("Unable to assign IP", e); + } + return null; + } + + @Override + public void unassignIpAddress(String ipAddress) { + IPAddressVO address = createForUpdate(); + address.setAccountId(null); + address.setDomainId(null); + address.setAllocated(null); + address.setSourceNat(false); + update(ipAddress, address); + } + + @Override + public void unassignIpAsSourceNat(String ipAddress) { + IPAddressVO address = createForUpdate(); + address.setSourceNat(false); + update(ipAddress, address); + } + + @Override + public List listByAccount(long accountId) { + SearchCriteria sc = AccountSearch.create(); + sc.setParameters("accountId", accountId); + return listBy(sc); + } + + public List listByDcIdIpAddress(long dcId, String ipAddress) { + SearchCriteria sc = DcIpSearch.create(); + sc.setParameters("dataCenterId", dcId); + sc.setParameters("ipAddress", ipAddress); + return listBy(sc); + } + + @Override @DB + public int countIPs(long dcId, long vlanDbId, boolean onlyCountAllocated) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + int ipCount = 0; + try { + String sql = "SELECT count(*) from `cloud`.`user_ip_address` where data_center_id = " + dcId; + + if (vlanDbId != -1) { + sql += " AND vlan_db_id = " + vlanDbId; + } + + if (onlyCountAllocated) { + sql += " AND allocated IS NOT NULL"; + } + + pstmt = txn.prepareAutoCloseStatement(sql); + ResultSet rs = pstmt.executeQuery(); + + if (rs.next()) { + ipCount = rs.getInt(1); + } + + } catch (Exception e) { + s_logger.warn("Exception counting IP addresses", e); + } + + return ipCount; + } + + @Override @DB + public int countIPs(long dcId, String vlanId, String vlanGateway, String vlanNetmask, boolean onlyCountAllocated) { + Transaction txn = Transaction.currentTxn(); + int ipCount = 0; + try { + String sql = "SELECT count(*) FROM user_ip_address u INNER JOIN vlan v on (u.vlan_db_id = v.id AND v.data_center_id = ? AND v.vlan_id = ? AND v.vlan_gateway = ? AND v.vlan_netmask = ?)"; + + if (onlyCountAllocated) { + sql += " WHERE allocated IS NOT NULL"; + } + + PreparedStatement pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, dcId); + pstmt.setString(2, vlanId); + pstmt.setString(3, vlanGateway); + pstmt.setString(4, vlanNetmask); + ResultSet rs = pstmt.executeQuery(); + + if (rs.next()) { + ipCount = rs.getInt(1); + } + } catch (Exception e) { + s_logger.warn("Exception counting IP addresses", e); + } + + return ipCount; + } +} diff --git a/core/src/com/cloud/network/dao/LoadBalancerDao.java b/core/src/com/cloud/network/dao/LoadBalancerDao.java new file mode 100644 index 00000000000..90822effa4e --- /dev/null +++ b/core/src/com/cloud/network/dao/LoadBalancerDao.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.List; + +import com.cloud.network.LoadBalancerVO; +import com.cloud.utils.db.GenericDao; + +public interface LoadBalancerDao extends GenericDao { + List listInstancesByLoadBalancer(long loadBalancerId); + List listByIpAddress(String ipAddress); + LoadBalancerVO findByIpAddressAndPublicPort(String ipAddress, String publicPort); + LoadBalancerVO findByAccountAndName(Long accountId, String name); +} diff --git a/core/src/com/cloud/network/dao/LoadBalancerDaoImpl.java b/core/src/com/cloud/network/dao/LoadBalancerDaoImpl.java new file mode 100644 index 00000000000..25d5eb42b33 --- /dev/null +++ b/core/src/com/cloud/network/dao/LoadBalancerDaoImpl.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.network.LoadBalancerVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value={LoadBalancerDao.class}) +public class LoadBalancerDaoImpl extends GenericDaoBase implements LoadBalancerDao { + private static final Logger s_logger = Logger.getLogger(LoadBalancerDaoImpl.class); + private static final String LIST_INSTANCES_BY_LOAD_BALANCER = "SELECT vm.id " + + " FROM vm_instance vm, load_balancer lb, ip_forwarding fwd, user_ip_address ip " + + " WHERE lb.id = ? AND " + + " fwd.group_id = lb.id AND " + + " fwd.forwarding = 0 AND " + + " fwd.private_ip_address = vm.private_ip_address AND " + + " lb.ip_address = ip.public_ip_address AND " + + " ip.data_center_id = vm.data_center_id "; + private final SearchBuilder ListByIp; + private final SearchBuilder IpAndPublicPortSearch; + private final SearchBuilder AccountAndNameSearch; + + protected LoadBalancerDaoImpl() { + ListByIp = createSearchBuilder(); + ListByIp.and("ipAddress", ListByIp.entity().getIpAddress(), SearchCriteria.Op.EQ); + ListByIp.done(); + + IpAndPublicPortSearch = createSearchBuilder(); + IpAndPublicPortSearch.and("ipAddress", IpAndPublicPortSearch.entity().getIpAddress(), SearchCriteria.Op.EQ); + IpAndPublicPortSearch.and("publicPort", IpAndPublicPortSearch.entity().getPublicPort(), SearchCriteria.Op.EQ); + IpAndPublicPortSearch.done(); + + AccountAndNameSearch = createSearchBuilder(); + AccountAndNameSearch.and("accountId", AccountAndNameSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountAndNameSearch.and("name", AccountAndNameSearch.entity().getName(), SearchCriteria.Op.EQ); + AccountAndNameSearch.done(); + } + + @Override + public List listInstancesByLoadBalancer(long loadBalancerId) { + Transaction txn = Transaction.currentTxn(); + String sql = LIST_INSTANCES_BY_LOAD_BALANCER; + PreparedStatement pstmt = null; + List instanceList = new ArrayList(); + try { + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, loadBalancerId); + + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + Long vmId = rs.getLong(1); + instanceList.add(vmId); + } + } catch (Exception ex) { + s_logger.error("error getting recent usage network stats", ex); + } + return instanceList; + } + + @Override + public List listByIpAddress(String ipAddress) { + SearchCriteria sc = ListByIp.create(); + sc.setParameters("ipAddress", ipAddress); + return listActiveBy(sc); + } + + @Override + public LoadBalancerVO findByIpAddressAndPublicPort(String ipAddress, String publicPort) { + SearchCriteria sc = IpAndPublicPortSearch.create(); + sc.setParameters("ipAddress", ipAddress); + sc.setParameters("publicPort", publicPort); + return findOneActiveBy(sc); + } + + @Override + public LoadBalancerVO findByAccountAndName(Long accountId, String name) { + SearchCriteria sc = AccountAndNameSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("name", name); + return findOneActiveBy(sc); + } +} diff --git a/core/src/com/cloud/network/dao/LoadBalancerVMMapDao.java b/core/src/com/cloud/network/dao/LoadBalancerVMMapDao.java new file mode 100644 index 00000000000..20db7c322b4 --- /dev/null +++ b/core/src/com/cloud/network/dao/LoadBalancerVMMapDao.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.List; + +import com.cloud.network.LoadBalancerVMMapVO; +import com.cloud.utils.db.GenericDao; + +public interface LoadBalancerVMMapDao extends GenericDao { + void remove(long loadBalancerId); + void remove(long loadBalancerId, List instanceIds, Boolean pending); + List listByInstanceId(long instanceId); + List listByLoadBalancerId(long loadBalancerId); + List listByLoadBalancerId(long loadBalancerId, boolean pending); +} diff --git a/core/src/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java b/core/src/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java new file mode 100644 index 00000000000..f82dbbb7c17 --- /dev/null +++ b/core/src/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.network.LoadBalancerVMMapVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={LoadBalancerVMMapDao.class}) +public class LoadBalancerVMMapDaoImpl extends GenericDaoBase implements LoadBalancerVMMapDao { + + @Override + public void remove(long loadBalancerId) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); + + delete(sc); + } + + @Override + public void remove(long loadBalancerId, List instanceIds, Boolean pending) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); + sc.addAnd("instanceId", SearchCriteria.Op.IN, instanceIds.toArray()); + if (pending != null) { + sc.addAnd("pending", SearchCriteria.Op.EQ, pending); + } + + delete(sc); + } + + @Override + public List listByInstanceId(long instanceId) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("instanceId", SearchCriteria.Op.EQ, instanceId); + + return listActiveBy(sc); + } + + @Override + public List listByLoadBalancerId(long loadBalancerId) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); + + return listActiveBy(sc); + } + + @Override + public List listByLoadBalancerId(long loadBalancerId, boolean pending) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); + sc.addAnd("pending", SearchCriteria.Op.EQ, pending); + + return listActiveBy(sc); + } +} diff --git a/core/src/com/cloud/network/dao/NetworkRuleConfigDao.java b/core/src/com/cloud/network/dao/NetworkRuleConfigDao.java new file mode 100644 index 00000000000..6511c8f2ca1 --- /dev/null +++ b/core/src/com/cloud/network/dao/NetworkRuleConfigDao.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.List; + +import com.cloud.network.NetworkRuleConfigVO; +import com.cloud.utils.db.GenericDao; + +public interface NetworkRuleConfigDao extends GenericDao { + List listBySecurityGroupId(long securityGroupId); + void deleteBySecurityGroup(long securityGroupId); +} diff --git a/core/src/com/cloud/network/dao/NetworkRuleConfigDaoImpl.java b/core/src/com/cloud/network/dao/NetworkRuleConfigDaoImpl.java new file mode 100644 index 00000000000..1e56e3b5181 --- /dev/null +++ b/core/src/com/cloud/network/dao/NetworkRuleConfigDaoImpl.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.network.NetworkRuleConfigVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={NetworkRuleConfigDao.class}) +public class NetworkRuleConfigDaoImpl extends GenericDaoBase implements NetworkRuleConfigDao { + protected SearchBuilder SecurityGroupIdSearch; + + protected NetworkRuleConfigDaoImpl() { + SecurityGroupIdSearch = createSearchBuilder(); + SecurityGroupIdSearch.and("securityGroupId", SecurityGroupIdSearch.entity().getSecurityGroupId(), SearchCriteria.Op.EQ); + SecurityGroupIdSearch.done(); + } + + public List listBySecurityGroupId(long securityGroupId) { + SearchCriteria sc = SecurityGroupIdSearch.create(); + sc.setParameters("securityGroupId", securityGroupId); + return listActiveBy(sc); + } + + public void deleteBySecurityGroup(long securityGroupId) { + SearchCriteria sc = SecurityGroupIdSearch.create(); + sc.setParameters("securityGroupId", securityGroupId); + delete(sc); + } +} diff --git a/core/src/com/cloud/network/dao/SecurityGroupDao.java b/core/src/com/cloud/network/dao/SecurityGroupDao.java new file mode 100644 index 00000000000..af9f8878ea7 --- /dev/null +++ b/core/src/com/cloud/network/dao/SecurityGroupDao.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.List; + +import com.cloud.network.SecurityGroupVO; +import com.cloud.utils.db.GenericDao; + +public interface SecurityGroupDao extends GenericDao { + List listByAccountId(long accountId); + boolean isNameInUse(Long accountId, Long domainId, String name); + List listAvailableGroups(Long accountId, Long domainId); +} diff --git a/core/src/com/cloud/network/dao/SecurityGroupDaoImpl.java b/core/src/com/cloud/network/dao/SecurityGroupDaoImpl.java new file mode 100644 index 00000000000..3e3b68498a7 --- /dev/null +++ b/core/src/com/cloud/network/dao/SecurityGroupDaoImpl.java @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.ArrayList; +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.network.SecurityGroupVO; +import com.cloud.server.ManagementServer; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={SecurityGroupDao.class}) +public class SecurityGroupDaoImpl extends GenericDaoBase implements SecurityGroupDao { + private SearchBuilder AccountIdSearch; + private DomainDao _domainDao = null; + + protected SecurityGroupDaoImpl() { + AccountIdSearch = createSearchBuilder(); + AccountIdSearch.and("accountId", AccountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountIdSearch.done(); + + _domainDao = ComponentLocator.getLocator(ManagementServer.Name).getDao(DomainDao.class); + } + + @Override + public List listByAccountId(long accountId) { + SearchCriteria sc = AccountIdSearch.create(); + sc.setParameters("accountId", accountId); + return listActiveBy(sc); + } + + @Override + public boolean isNameInUse(Long accountId, Long domainId, String name) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("name", SearchCriteria.Op.EQ, name); + if (accountId != null) { + sc.addAnd("accountId", SearchCriteria.Op.EQ, accountId); + } else { + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + sc.addAnd("accountId", SearchCriteria.Op.NULL); + } + + List securityGroups = listActiveBy(sc); + return ((securityGroups != null) && !securityGroups.isEmpty()); + } + + @Override + public List listAvailableGroups(Long accountId, Long domainId) { + List availableGroups = new ArrayList(); + if ((accountId != null) || (domainId != null)) { + if (accountId != null) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("accountId", SearchCriteria.Op.EQ, accountId); + List accountGroups = listActiveBy(sc); + availableGroups.addAll(accountGroups); + } else if (domainId != null) { + while (domainId != null) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + if (accountId != null) { + sc.addAnd("accountId", SearchCriteria.Op.NEQ, accountId); // we added the account specific ones above + } + List domainGroups = listActiveBy(sc); + availableGroups.addAll(domainGroups); + + // get the parent domain, repeat the loop + DomainVO domain = _domainDao.findById(domainId); + domainId = domain.getParent(); + } + } + } + return availableGroups; + } +} diff --git a/core/src/com/cloud/network/dao/SecurityGroupVMMapDao.java b/core/src/com/cloud/network/dao/SecurityGroupVMMapDao.java new file mode 100644 index 00000000000..eabea48fe8a --- /dev/null +++ b/core/src/com/cloud/network/dao/SecurityGroupVMMapDao.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.List; + +import com.cloud.network.SecurityGroupVMMapVO; +import com.cloud.utils.db.GenericDao; + +public interface SecurityGroupVMMapDao extends GenericDao { + List listByIpAndInstanceId(String ipAddress, long instanceId); + List listByInstanceId(long instanceId); + List listByIp(String ipAddress); + List listBySecurityGroup(long securityGroupId); +} diff --git a/core/src/com/cloud/network/dao/SecurityGroupVMMapDaoImpl.java b/core/src/com/cloud/network/dao/SecurityGroupVMMapDaoImpl.java new file mode 100644 index 00000000000..c8da6f4da18 --- /dev/null +++ b/core/src/com/cloud/network/dao/SecurityGroupVMMapDaoImpl.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.network.SecurityGroupVMMapVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={SecurityGroupVMMapDao.class}) +public class SecurityGroupVMMapDaoImpl extends GenericDaoBase implements SecurityGroupVMMapDao { + private SearchBuilder ListByIpAndVmId; + private SearchBuilder ListByVmId; + private SearchBuilder ListByIp; + private SearchBuilder ListBySecurityGroup; + + protected SecurityGroupVMMapDaoImpl() { + ListByIpAndVmId = createSearchBuilder(); + ListByIpAndVmId.and("ipAddress", ListByIpAndVmId.entity().getIpAddress(), SearchCriteria.Op.EQ); + ListByIpAndVmId.and("instanceId", ListByIpAndVmId.entity().getInstanceId(), SearchCriteria.Op.EQ); + ListByIpAndVmId.done(); + + ListBySecurityGroup = createSearchBuilder(); + ListBySecurityGroup.and("securityGroupId", ListBySecurityGroup.entity().getSecurityGroupId(), SearchCriteria.Op.EQ); + ListBySecurityGroup.done(); + + ListByIp = createSearchBuilder(); + ListByIp.and("ipAddress", ListByIp.entity().getIpAddress(), SearchCriteria.Op.EQ); + ListByIp.done(); + + ListByVmId = createSearchBuilder(); + ListByVmId.and("instanceId", ListByVmId.entity().getInstanceId(), SearchCriteria.Op.EQ); + ListByVmId.done(); + } + + public List listByIpAndInstanceId(String ipAddress, long vmId) { + SearchCriteria sc = ListByIpAndVmId.create(); + sc.setParameters("ipAddress", ipAddress); + sc.setParameters("instanceId", vmId); + return listActiveBy(sc); + } + + @Override + public List listBySecurityGroup(long securityGroupId) { + SearchCriteria sc = ListBySecurityGroup.create(); + sc.setParameters("securityGroupId", securityGroupId); + return listActiveBy(sc); + } + + @Override + public List listByIp(String ipAddress) { + SearchCriteria sc = ListByIp.create(); + sc.setParameters("ipAddress", ipAddress); + return listActiveBy(sc); + } + + @Override + public List listByInstanceId(long vmId) { + SearchCriteria sc = ListByVmId.create(); + sc.setParameters("instanceId", vmId); + return listActiveBy(sc); + } + +} diff --git a/core/src/com/cloud/network/security/IngressRuleVO.java b/core/src/com/cloud/network/security/IngressRuleVO.java new file mode 100644 index 00000000000..1f15f28200b --- /dev/null +++ b/core/src/com/cloud/network/security/IngressRuleVO.java @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.async.AsyncInstanceCreateStatus; +import com.google.gson.annotations.Expose; + +@Entity +@Table(name=("network_ingress_rule")) +public class IngressRuleVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="network_group_id") + private long networkGroupId; + + @Column(name="start_port") + private int startPort; + + @Column(name="end_port") + private int endPort; + + @Column(name="protocol") + private String protocol; + + @Column(name="allowed_network_id", nullable=true) + private Long allowedNetworkId = null; + + @Column(name="allowed_network_group") + private String allowedNetworkGroup; + + @Column(name="allowed_net_grp_acct") + private String allowedNetGrpAcct; + + @Column(name="allowed_ip_cidr", nullable=true) + private String allowedSourceIpCidr = null; + + @Expose + @Column(name="create_status", updatable = true, nullable=false) + @Enumerated(value=EnumType.STRING) + private AsyncInstanceCreateStatus createStatus; + + public IngressRuleVO() {} + + public IngressRuleVO(long networkGroupId, int fromPort, int toPort, String protocol, long allowedNetworkId, String allowedNetworkGroup, String allowedNetGrpAcct) { + this.networkGroupId = networkGroupId; + this.startPort = fromPort; + this.endPort = toPort; + this.protocol = protocol; + this.allowedNetworkId = allowedNetworkId; + this.allowedNetworkGroup = allowedNetworkGroup; + this.allowedNetGrpAcct = allowedNetGrpAcct; + } + + public IngressRuleVO(long networkGroupId, int fromPort, int toPort, String protocol, String allowedIpCidr) { + this.networkGroupId = networkGroupId; + this.startPort = fromPort; + this.endPort = toPort; + this.protocol = protocol; + this.allowedSourceIpCidr = allowedIpCidr; + } + + public Long getId() { + return id; + } + + public long getNetworkGroupId() { + return networkGroupId; + } + + public int getStartPort() { + return startPort; + } + + public int getEndPort() { + return endPort; + } + + public String getProtocol() { + return protocol; + } + + public AsyncInstanceCreateStatus getCreateStatus() { + return createStatus; + } + + public void setCreateStatus(AsyncInstanceCreateStatus createStatus) { + this.createStatus = createStatus; + } + + public Long getAllowedNetworkId() { + return allowedNetworkId; + } + + public String getAllowedNetworkGroup() { + return allowedNetworkGroup; + } + + public String getAllowedNetGrpAcct() { + return allowedNetGrpAcct; + } + + public String getAllowedSourceIpCidr() { + return allowedSourceIpCidr; + } +} diff --git a/core/src/com/cloud/network/security/NetworkGroupRulesVO.java b/core/src/com/cloud/network/security/NetworkGroupRulesVO.java new file mode 100644 index 00000000000..cffd84eed42 --- /dev/null +++ b/core/src/com/cloud/network/security/NetworkGroupRulesVO.java @@ -0,0 +1,135 @@ +package com.cloud.network.security; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.SecondaryTable; +import javax.persistence.Table; + +@Entity +@Table(name=("network_group")) +@SecondaryTable(name="network_ingress_rule", join="left", + pkJoinColumns={@PrimaryKeyJoinColumn(name="id", referencedColumnName="network_group_id")}) +public class NetworkGroupRulesVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="name") + private String name; + + @Column(name="description") + private String description; + + @Column(name="domain_id") + private Long domainId; + + @Column(name="account_id") + private Long accountId; + + @Column(name="account_name") + private String accountName; + + @Column(name="id", table="network_ingress_rule", insertable=false, updatable=false) + private Long ruleId; + + @Column(name="start_port", table="network_ingress_rule", insertable=false, updatable=false) + private int startPort; + + @Column(name="end_port", table="network_ingress_rule", insertable=false, updatable=false) + private int endPort; + + @Column(name="protocol", table="network_ingress_rule", insertable=false, updatable=false) + private String protocol; + + @Column(name="allowed_network_id", table="network_ingress_rule", insertable=false, updatable=false, nullable=true) + private Long allowedNetworkId = null; + + @Column(name="allowed_network_group", table="network_ingress_rule", insertable=false, updatable=false, nullable=true) + private String allowedNetworkGroup = null; + + @Column(name="allowed_net_grp_acct", table="network_ingress_rule", insertable=false, updatable=false, nullable=true) + private String allowedNetGrpAcct = null; + + @Column(name="allowed_ip_cidr", table="network_ingress_rule", insertable=false, updatable=false, nullable=true) + private String allowedSourceIpCidr = null; + + public NetworkGroupRulesVO() { } + + public NetworkGroupRulesVO(Long id, String name, String description, Long domainId, Long accountId, String accountName, Long ruleId, int startPort, int endPort, String protocol, Long allowedNetworkId, String allowedNetworkGroup, String allowedNetGrpAcct, String allowedSourceIpCidr) { + this.id = id; + this.name = name; + this.description = description; + this.domainId = domainId; + this.accountId = accountId; + this.accountName = accountName; + this.ruleId = ruleId; + this.startPort = startPort; + this.endPort = endPort; + this.protocol = protocol; + this.allowedNetworkId = allowedNetworkId; + this.allowedNetworkGroup = allowedNetworkGroup; + this.allowedNetGrpAcct = allowedNetGrpAcct; + this.allowedSourceIpCidr = allowedSourceIpCidr; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Long getDomainId() { + return domainId; + } + + public Long getAccountId() { + return accountId; + } + + public String getAccountName() { + return accountName; + } + + public Long getRuleId() { + return ruleId; + } + + public int getStartPort() { + return startPort; + } + + public int getEndPort() { + return endPort; + } + + public String getProtocol() { + return protocol; + } + + public Long getAllowedNetworkId() { + return allowedNetworkId; + } + + public String getAllowedNetworkGroup() { + return allowedNetworkGroup; + } + + public String getAllowedNetGrpAcct() { + return allowedNetGrpAcct; + } + + public String getAllowedSourceIpCidr() { + return allowedSourceIpCidr; + } +} diff --git a/core/src/com/cloud/network/security/NetworkGroupVMMapVO.java b/core/src/com/cloud/network/security/NetworkGroupVMMapVO.java new file mode 100644 index 00000000000..29c793618b1 --- /dev/null +++ b/core/src/com/cloud/network/security/NetworkGroupVMMapVO.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.SecondaryTable; +import javax.persistence.SecondaryTables; +import javax.persistence.Table; + +import com.cloud.vm.State; + +@Entity +@Table(name=("network_group_vm_map")) +@SecondaryTables({ +@SecondaryTable(name="user_vm", + pkJoinColumns={@PrimaryKeyJoinColumn(name="instance_id", referencedColumnName="id")}), +@SecondaryTable(name="vm_instance", + pkJoinColumns={@PrimaryKeyJoinColumn(name="instance_id", referencedColumnName="id")}), +@SecondaryTable(name="network_group", + pkJoinColumns={@PrimaryKeyJoinColumn(name="network_group_id", referencedColumnName="id")}) + }) +public class NetworkGroupVMMapVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="network_group_id") + private long networkGroupId; + + @Column(name="instance_id") + private long instanceId; + + @Column(name="guest_ip_address", table="user_vm", insertable=false, updatable=false) + private String guestIpAddress; + + @Column(name="state", table="vm_instance", insertable=false, updatable=false) + private State vmState; + + @Column(name="name", table="network_group", insertable=false, updatable=false) + private String groupName; + + public NetworkGroupVMMapVO() { } + + public NetworkGroupVMMapVO(long networkGroupId, long instanceId) { + this.networkGroupId = networkGroupId; + this.instanceId = instanceId; + } + + public Long getId() { + return id; + } + + public long getNetworkGroupId() { + return networkGroupId; + } + + public String getGuestIpAddress() { + return guestIpAddress; + } + + public long getInstanceId() { + return instanceId; + } + + public State getVmState() { + return vmState; + } + + public String getGroupName() { + return groupName; + } +} diff --git a/core/src/com/cloud/network/security/NetworkGroupVO.java b/core/src/com/cloud/network/security/NetworkGroupVO.java new file mode 100644 index 00000000000..d20823bb035 --- /dev/null +++ b/core/src/com/cloud/network/security/NetworkGroupVO.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.SecondaryTable; +import javax.persistence.Table; + +@Entity +@Table(name=("network_group")) +public class NetworkGroupVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="name") + private String name; + + @Column(name="description") + private String description; + + @Column(name="domain_id") + private Long domainId; + + @Column(name="account_id") + private Long accountId; + + @Column(name="account_name") + private String accountName = null; + + public NetworkGroupVO() {} + + public NetworkGroupVO(String name, String description, Long domainId, Long accountId, String accountName) { + this.name = name; + this.description = description; + this.domainId = domainId; + this.accountId = accountId; + this.accountName = accountName; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Long getDomainId() { + return domainId; + } + + public Long getAccountId() { + return accountId; + } + + public String getAccountName() { + return accountName; + } +} diff --git a/core/src/com/cloud/network/security/NetworkGroupWorkVO.java b/core/src/com/cloud/network/security/NetworkGroupWorkVO.java new file mode 100644 index 00000000000..4f58478345c --- /dev/null +++ b/core/src/com/cloud/network/security/NetworkGroupWorkVO.java @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.network.security; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="op_nwgrp_work") +public class NetworkGroupWorkVO { + public enum Step { + Scheduled, + Processing, + Done, + Error + } + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="instance_id", updatable=false, nullable=false) + private Long instanceId; // vm_instance id + + + @Column(name="mgmt_server_id", nullable=true) + private Long serverId; + + @Column(name=GenericDao.CREATED_COLUMN) + private Date created; + + + @Column(name="step", nullable = false) + @Enumerated(value=EnumType.STRING) + private Step step; + + @Column(name="taken", nullable=true) + @Temporal(value=TemporalType.TIMESTAMP) + private Date dateTaken; + + @Column(name="seq_no", nullable=true) + private Long logsequenceNumber = null; + + + protected NetworkGroupWorkVO() { + } + + public Long getId() { + return id; + } + + public Long getInstanceId() { + return instanceId; + } + + + public Long getServerId() { + return serverId; + } + + + public void setServerId(final Long serverId) { + this.serverId = serverId; + } + + public Date getCreated() { + return created; + } + + + + public NetworkGroupWorkVO(Long instanceId, Long serverId, Date created, + Step step, Date dateTaken) { + super(); + this.instanceId = instanceId; + this.serverId = serverId; + this.created = created; + this.step = step; + this.dateTaken = dateTaken; + } + + @Override + public String toString() { + return new StringBuilder("[NWGrp-Work:id=").append(id).append(":vm=").append(instanceId).append("]").toString(); + } + + public Date getDateTaken() { + return dateTaken; + } + + public void setStep(Step step) { + this.step = step; + } + + public Step getStep() { + return step; + } + + public void setDateTaken(Date date) { + dateTaken = date; + } + + public Long getLogsequenceNumber() { + return logsequenceNumber; + } + + public void setLogsequenceNumber(Long logsequenceNumber) { + this.logsequenceNumber = logsequenceNumber; + } + +} diff --git a/core/src/com/cloud/network/security/VmRulesetLogVO.java b/core/src/com/cloud/network/security/VmRulesetLogVO.java new file mode 100644 index 00000000000..fe017a874d1 --- /dev/null +++ b/core/src/com/cloud/network/security/VmRulesetLogVO.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2010 Cloud.com. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later +version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.network.security; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.utils.db.GenericDao; + +/** + * Records the intent to update a VM's ingress ruleset + * + */ +@Entity +@Table(name="op_vm_ruleset_log") +public class VmRulesetLogVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="instance_id", updatable=false, nullable=false) + private Long instanceId; // vm_instance id + + @Column(name=GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name="logsequence") + long logsequence; + + protected VmRulesetLogVO() { + + } + + public VmRulesetLogVO(Long instanceId) { + super(); + this.instanceId = instanceId; + } + + public Long getId() { + return id; + } + + public Long getInstanceId() { + return instanceId; + } + + public Date getCreated() { + return created; + } + + public long getLogsequence() { + return logsequence; + } + + public void incrLogsequence() { + logsequence++; + } + +} diff --git a/core/src/com/cloud/network/security/dao/IngressRuleDao.java b/core/src/com/cloud/network/security/dao/IngressRuleDao.java new file mode 100644 index 00000000000..859e82ddaa5 --- /dev/null +++ b/core/src/com/cloud/network/security/dao/IngressRuleDao.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security.dao; + +import java.util.List; + +import com.cloud.network.security.IngressRuleVO; +import com.cloud.utils.db.GenericDao; + +public interface IngressRuleDao extends GenericDao { + List listByNetworkGroupId(long networkGroupId); + List listByAllowedNetworkGroupId(long networkGroupId); + IngressRuleVO findByProtoPortsAndCidr(long networkGroupId, String proto, int startPort, int endPort, String cidr); + IngressRuleVO findByProtoPortsAndGroup(String proto, int startPort, int endPort, String networkGroup); + IngressRuleVO findByProtoPortsAndAllowedGroupId(long networkGroupId, String proto, int startPort, int endPort, Long allowedGroupId); + int deleteByNetworkGroup(long networkGroupId); + int deleteByPortProtoAndGroup(long networkGroupId, String protocol, int startPort,int endPort, Long id); + int deleteByPortProtoAndCidr(long networkGroupId, String protocol, int startPort,int endPort, String cidr); + +} diff --git a/core/src/com/cloud/network/security/dao/IngressRuleDaoImpl.java b/core/src/com/cloud/network/security/dao/IngressRuleDaoImpl.java new file mode 100644 index 00000000000..825630c1e43 --- /dev/null +++ b/core/src/com/cloud/network/security/dao/IngressRuleDaoImpl.java @@ -0,0 +1,166 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security.dao; + +import java.util.List; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import com.cloud.network.security.IngressRuleVO; +import com.cloud.network.security.NetworkGroupVO; +import com.cloud.utils.component.Inject; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={IngressRuleDao.class}) +public class IngressRuleDaoImpl extends GenericDaoBase implements IngressRuleDao { + + @Inject NetworkGroupDao _networkGroupDao; + + protected SearchBuilder networkGroupIdSearch; + protected SearchBuilder allowedNetworkGroupIdSearch; + protected SearchBuilder protoPortsAndCidrSearch; + protected SearchBuilder protoPortsAndNetworkGroupNameSearch; + protected SearchBuilder protoPortsAndNetworkGroupIdSearch; + + + + protected IngressRuleDaoImpl() { + networkGroupIdSearch = createSearchBuilder(); + networkGroupIdSearch.and("networkGroupId", networkGroupIdSearch.entity().getNetworkGroupId(), SearchCriteria.Op.EQ); + networkGroupIdSearch.done(); + + allowedNetworkGroupIdSearch = createSearchBuilder(); + allowedNetworkGroupIdSearch.and("allowedNetworkId", allowedNetworkGroupIdSearch.entity().getAllowedNetworkId(), SearchCriteria.Op.EQ); + allowedNetworkGroupIdSearch.done(); + + protoPortsAndCidrSearch = createSearchBuilder(); + protoPortsAndCidrSearch.and("networkGroupId", protoPortsAndCidrSearch.entity().getNetworkGroupId(), SearchCriteria.Op.EQ); + protoPortsAndCidrSearch.and("proto", protoPortsAndCidrSearch.entity().getProtocol(), SearchCriteria.Op.EQ); + protoPortsAndCidrSearch.and("startPort", protoPortsAndCidrSearch.entity().getStartPort(), SearchCriteria.Op.EQ); + protoPortsAndCidrSearch.and("endPort", protoPortsAndCidrSearch.entity().getEndPort(), SearchCriteria.Op.EQ); + protoPortsAndCidrSearch.and("cidr", protoPortsAndCidrSearch.entity().getAllowedSourceIpCidr(), SearchCriteria.Op.EQ); + protoPortsAndCidrSearch.done(); + + protoPortsAndNetworkGroupIdSearch = createSearchBuilder(); + protoPortsAndNetworkGroupIdSearch.and("networkGroupId", protoPortsAndNetworkGroupIdSearch.entity().getNetworkGroupId(), SearchCriteria.Op.EQ); + protoPortsAndNetworkGroupIdSearch.and("proto", protoPortsAndNetworkGroupIdSearch.entity().getProtocol(), SearchCriteria.Op.EQ); + protoPortsAndNetworkGroupIdSearch.and("startPort", protoPortsAndNetworkGroupIdSearch.entity().getStartPort(), SearchCriteria.Op.EQ); + protoPortsAndNetworkGroupIdSearch.and("endPort", protoPortsAndNetworkGroupIdSearch.entity().getEndPort(), SearchCriteria.Op.EQ); + protoPortsAndNetworkGroupIdSearch.and("allowedNetworkId", protoPortsAndNetworkGroupIdSearch.entity().getAllowedNetworkId(), SearchCriteria.Op.EQ); + + } + + public List listByNetworkGroupId(long networkGroupId) { + SearchCriteria sc = networkGroupIdSearch.create(); + sc.setParameters("networkGroupId", networkGroupId); + return listActiveBy(sc); + } + + public int deleteByNetworkGroup(long networkGroupId) { + SearchCriteria sc = networkGroupIdSearch.create(); + sc.setParameters("networkGroupId", networkGroupId); + return delete(sc); + } + + @Override + public List listByAllowedNetworkGroupId(long networkGroupId) { + SearchCriteria sc = allowedNetworkGroupIdSearch.create(); + sc.setParameters("allowedNetworkId", networkGroupId); + return listActiveBy(sc); + } + + @Override + public IngressRuleVO findByProtoPortsAndCidr(long networkGroupId, String proto, int startPort, + int endPort, String cidr) { + SearchCriteria sc = protoPortsAndCidrSearch.create(); + sc.setParameters("networkGroupId", networkGroupId); + sc.setParameters("proto", proto); + sc.setParameters("startPort", startPort); + sc.setParameters("endPort", endPort); + sc.setParameters("cidr", cidr); + return findOneBy(sc); + } + + @Override + public IngressRuleVO findByProtoPortsAndGroup(String proto, int startPort, + int endPort, String networkGroup) { + SearchCriteria sc = protoPortsAndNetworkGroupNameSearch.create(); + sc.setParameters("proto", proto); + sc.setParameters("startPort", startPort); + sc.setParameters("endPort", endPort); + sc.setJoinParameters("groupName", "groupName", networkGroup); + return findOneBy(sc); + } + + @Override + public boolean configure(String name, Map params) + throws ConfigurationException { + protoPortsAndNetworkGroupNameSearch = createSearchBuilder(); + protoPortsAndNetworkGroupNameSearch.and("proto", protoPortsAndNetworkGroupNameSearch.entity().getProtocol(), SearchCriteria.Op.EQ); + protoPortsAndNetworkGroupNameSearch.and("startPort", protoPortsAndNetworkGroupNameSearch.entity().getStartPort(), SearchCriteria.Op.EQ); + protoPortsAndNetworkGroupNameSearch.and("endPort", protoPortsAndNetworkGroupNameSearch.entity().getEndPort(), SearchCriteria.Op.EQ); + SearchBuilder ngSb = _networkGroupDao.createSearchBuilder(); + ngSb.and("groupName", ngSb.entity().getName(), SearchCriteria.Op.EQ); + protoPortsAndNetworkGroupNameSearch.join("groupName", ngSb, protoPortsAndNetworkGroupNameSearch.entity().getAllowedNetworkId(), ngSb.entity().getId()); + protoPortsAndNetworkGroupNameSearch.done(); + return super.configure(name, params); + } + + @Override + public int deleteByPortProtoAndGroup(long networkGroupId, String protocol, int startPort, int endPort, Long allowedGroupId) { + SearchCriteria sc = protoPortsAndNetworkGroupIdSearch.create(); + sc.setParameters("networkGroupId", networkGroupId); + sc.setParameters("proto", protocol); + sc.setParameters("startPort", startPort); + sc.setParameters("endPort", endPort); + sc.setParameters("allowedNetworkId", allowedGroupId); + + return delete(sc); + + } + + @Override + public int deleteByPortProtoAndCidr(long networkGroupId, String protocol, int startPort, int endPort, String cidr) { + SearchCriteria sc = protoPortsAndCidrSearch.create(); + sc.setParameters("networkGroupId", networkGroupId); + sc.setParameters("proto", protocol); + sc.setParameters("startPort", startPort); + sc.setParameters("endPort", endPort); + sc.setParameters("cidr", cidr); + + return delete(sc); + } + + @Override + public IngressRuleVO findByProtoPortsAndAllowedGroupId(long networkGroupId, String proto, + int startPort, int endPort, Long allowedGroupId) { + SearchCriteria sc = protoPortsAndNetworkGroupIdSearch.create(); + sc.addAnd("networkGroupId", SearchCriteria.Op.EQ, networkGroupId); + sc.setParameters("proto", proto); + sc.setParameters("startPort", startPort); + sc.setParameters("endPort", endPort); + sc.setParameters("allowedNetworkId", allowedGroupId); + + return findOneBy(sc); + } +} diff --git a/core/src/com/cloud/network/security/dao/NetworkGroupDao.java b/core/src/com/cloud/network/security/dao/NetworkGroupDao.java new file mode 100644 index 00000000000..189538d32ed --- /dev/null +++ b/core/src/com/cloud/network/security/dao/NetworkGroupDao.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security.dao; + +import java.util.List; + +import com.cloud.network.security.NetworkGroupVO; +import com.cloud.utils.db.GenericDao; + +public interface NetworkGroupDao extends GenericDao { + List listByAccountId(long accountId); + boolean isNameInUse(Long accountId, Long domainId, String name); + List listAvailableGroups(Long accountId, Long domainId); + NetworkGroupVO findByAccountAndName(Long accountId, String name); + List findByAccountAndNames(Long accountId, String... names); +} diff --git a/core/src/com/cloud/network/security/dao/NetworkGroupDaoImpl.java b/core/src/com/cloud/network/security/dao/NetworkGroupDaoImpl.java new file mode 100644 index 00000000000..2513eba3b00 --- /dev/null +++ b/core/src/com/cloud/network/security/dao/NetworkGroupDaoImpl.java @@ -0,0 +1,129 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security.dao; + +import java.util.ArrayList; +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.network.security.NetworkGroupVO; +import com.cloud.server.ManagementServer; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={NetworkGroupDao.class}) +public class NetworkGroupDaoImpl extends GenericDaoBase implements NetworkGroupDao { + private SearchBuilder AccountIdSearch; + private SearchBuilder AccountIdNameSearch; + private SearchBuilder AccountIdNamesSearch; + + private DomainDao _domainDao = null; + + protected NetworkGroupDaoImpl() { + AccountIdSearch = createSearchBuilder(); + AccountIdSearch.and("accountId", AccountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountIdSearch.done(); + + AccountIdNameSearch = createSearchBuilder(); + AccountIdNameSearch.and("accountId", AccountIdNameSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountIdNameSearch.and("groupName", AccountIdNameSearch.entity().getName(), SearchCriteria.Op.EQ); + + AccountIdNamesSearch = createSearchBuilder(); + AccountIdNamesSearch.and("accountId", AccountIdNamesSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountIdNamesSearch.and("groupNames", AccountIdNamesSearch.entity().getName(), SearchCriteria.Op.IN); + + AccountIdNameSearch.done(); + + _domainDao = ComponentLocator.getLocator(ManagementServer.Name).getDao(DomainDao.class); + } + + @Override + public List listByAccountId(long accountId) { + SearchCriteria sc = AccountIdSearch.create(); + sc.setParameters("accountId", accountId); + return listActiveBy(sc); + } + + @Override + public boolean isNameInUse(Long accountId, Long domainId, String name) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("name", SearchCriteria.Op.EQ, name); + if (accountId != null) { + sc.addAnd("accountId", SearchCriteria.Op.EQ, accountId); + } else { + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + sc.addAnd("accountId", SearchCriteria.Op.NULL); + } + + List securityGroups = listActiveBy(sc); + return ((securityGroups != null) && !securityGroups.isEmpty()); + } + + @Override + public List listAvailableGroups(Long accountId, Long domainId) { + List availableGroups = new ArrayList(); + if ((accountId != null) || (domainId != null)) { + if (accountId != null) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("accountId", SearchCriteria.Op.EQ, accountId); + List accountGroups = listActiveBy(sc); + availableGroups.addAll(accountGroups); + } else if (domainId != null) { + while (domainId != null) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + if (accountId != null) { + sc.addAnd("accountId", SearchCriteria.Op.NEQ, accountId); // we added the account specific ones above + } + List domainGroups = listActiveBy(sc); + availableGroups.addAll(domainGroups); + + // get the parent domain, repeat the loop + DomainVO domain = _domainDao.findById(domainId); + domainId = domain.getParent(); + } + } + } + return availableGroups; + } + + @Override + public NetworkGroupVO findByAccountAndName(Long accountId, String name) { + SearchCriteria sc = AccountIdNameSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("groupName", name); + + return findOneBy(sc); + } + + @Override + public List findByAccountAndNames(Long accountId, String... names) { + SearchCriteria sc = AccountIdNamesSearch.create(); + sc.setParameters("accountId", accountId); + + sc.setParameters("groupNames", (Object [])names); + + return listActiveBy(sc); + } +} diff --git a/core/src/com/cloud/network/security/dao/NetworkGroupRulesDao.java b/core/src/com/cloud/network/security/dao/NetworkGroupRulesDao.java new file mode 100644 index 00000000000..2ef4d9fd387 --- /dev/null +++ b/core/src/com/cloud/network/security/dao/NetworkGroupRulesDao.java @@ -0,0 +1,37 @@ +package com.cloud.network.security.dao; + +import java.util.List; + +import com.cloud.network.security.NetworkGroupRulesVO; +import com.cloud.utils.db.GenericDao; + +public interface NetworkGroupRulesDao extends GenericDao { + /** + * List a network group and associated ingress rules + * @param accountId the account id of the owner of the network group + * @param groupName the name of the group for which to list rules + * @return the list of ingress rules associated with the network group (and network group info) + */ + List listNetworkGroupRules(long accountId, String groupName); + + /** + * List network groups and associated ingress rules + * @param accountId the id of the account for which to list groups and associated rules + * @return the list of network groups with associated ingress rules + */ + List listNetworkGroupRules(long accountId); + + /** + * List all network groups and associated ingress rules + * @return the list of network groups with associated ingress rules + */ + List listNetworkGroupRules(); + + /** + * List network groups and associated ingress rules for a particular domain + * @param domainId the id of the domain for which to list groups and associated rules + * @param recursive whether or not to recursively search the domain for network groups + * @return the list of network groups with associated ingress rules + */ + List listNetworkGroupRulesByDomain(long domainId, boolean recursive); +} diff --git a/core/src/com/cloud/network/security/dao/NetworkGroupRulesDaoImpl.java b/core/src/com/cloud/network/security/dao/NetworkGroupRulesDaoImpl.java new file mode 100644 index 00000000000..391771571a3 --- /dev/null +++ b/core/src/com/cloud/network/security/dao/NetworkGroupRulesDaoImpl.java @@ -0,0 +1,91 @@ +package com.cloud.network.security.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.network.security.NetworkGroupRulesVO; +import com.cloud.server.ManagementServer; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={NetworkGroupRulesDao.class}) +public class NetworkGroupRulesDaoImpl extends GenericDaoBase implements NetworkGroupRulesDao { + private SearchBuilder AccountGroupNameSearch; + private SearchBuilder AccountSearch; + private SearchBuilder DomainSearch; + + private DomainDao _domainDao = null; + + protected NetworkGroupRulesDaoImpl() { + AccountGroupNameSearch = createSearchBuilder(); + AccountGroupNameSearch.and("accountId", AccountGroupNameSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountGroupNameSearch.and("name", AccountGroupNameSearch.entity().getName(), SearchCriteria.Op.EQ); + AccountGroupNameSearch.done(); + + AccountSearch = createSearchBuilder(); + AccountSearch.and("accountId", AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountSearch.done(); + } + + @Override + public List listNetworkGroupRules() { + Filter searchFilter = new Filter(NetworkGroupRulesVO.class, "id", true, null, null); + return listAllActive(searchFilter); + } + + @Override + public List listNetworkGroupRules(long accountId, String groupName) { + Filter searchFilter = new Filter(NetworkGroupRulesVO.class, "id", true, null, null); + + SearchCriteria sc = AccountGroupNameSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("name", groupName); + + return listActiveBy(sc, searchFilter); + } + + @Override + public List listNetworkGroupRules(long accountId) { + Filter searchFilter = new Filter(NetworkGroupRulesVO.class, "id", true, null, null); + SearchCriteria sc = AccountSearch.create(); + sc.setParameters("accountId", accountId); + + return listActiveBy(sc, searchFilter); + } + + @Override + public List listNetworkGroupRulesByDomain(long domainId, boolean recursive) { + + if (_domainDao == null) { + ComponentLocator locator = ComponentLocator.getLocator(ManagementServer.Name); + _domainDao = locator.getDao(DomainDao.class); + + DomainSearch = createSearchBuilder(); + DomainSearch.and("domainId", DomainSearch.entity().getDomainId(), SearchCriteria.Op.EQ); + SearchBuilder domainSearch = _domainDao.createSearchBuilder(); + domainSearch.and("path", domainSearch.entity().getPath(), SearchCriteria.Op.LIKE); + DomainSearch.join("domainSearch", domainSearch, DomainSearch.entity().getDomainId(), domainSearch.entity().getId()); + DomainSearch.done(); + } + + Filter searchFilter = new Filter(NetworkGroupRulesVO.class, "id", true, null, null); + SearchCriteria sc = DomainSearch.create(); + + if (!recursive) { + sc.setParameters("domainId", domainId); + } + + DomainVO domain = _domainDao.findById(domainId); + if (domain != null) { + sc.setJoinParameters("domainSearch", "path", domain.getPath() + "%"); + } + + return listActiveBy(sc, searchFilter); + } +} diff --git a/core/src/com/cloud/network/security/dao/NetworkGroupVMMapDao.java b/core/src/com/cloud/network/security/dao/NetworkGroupVMMapDao.java new file mode 100644 index 00000000000..f7f7cb2ba88 --- /dev/null +++ b/core/src/com/cloud/network/security/dao/NetworkGroupVMMapDao.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security.dao; + +import java.util.List; + +import com.cloud.network.security.NetworkGroupVMMapVO; +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.State; + +public interface NetworkGroupVMMapDao extends GenericDao { + List listByIpAndInstanceId(String ipAddress, long instanceId); + List listByInstanceId(long instanceId); + List listByIp(String ipAddress); + List listByNetworkGroup(long networkGroupId); + List listByNetworkGroup(long networkGroupId, State ... vmStates); + int deleteVM(long instanceid); + List listVmIdsByNetworkGroup(long networkGroupId); + NetworkGroupVMMapVO findByVmIdGroupId(long instanceId, long networkGroupId); +} diff --git a/core/src/com/cloud/network/security/dao/NetworkGroupVMMapDaoImpl.java b/core/src/com/cloud/network/security/dao/NetworkGroupVMMapDaoImpl.java new file mode 100644 index 00000000000..fd985e134c8 --- /dev/null +++ b/core/src/com/cloud/network/security/dao/NetworkGroupVMMapDaoImpl.java @@ -0,0 +1,136 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.network.security.NetworkGroupVMMapVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.State; + +@Local(value={NetworkGroupVMMapDao.class}) +public class NetworkGroupVMMapDaoImpl extends GenericDaoBase implements NetworkGroupVMMapDao { + private SearchBuilder ListByIpAndVmId; + private SearchBuilder ListByVmId; + private SearchBuilder ListByVmIdGroupId; + + private GenericSearchBuilder ListVmIdByNetworkGroup; + + private SearchBuilder ListByIp; + private SearchBuilder ListByNetworkGroup; + private SearchBuilder ListByNetworkGroupAndStates; + + protected NetworkGroupVMMapDaoImpl() { + ListByIpAndVmId = createSearchBuilder(); + ListByIpAndVmId.and("ipAddress", ListByIpAndVmId.entity().getGuestIpAddress(), SearchCriteria.Op.EQ); + ListByIpAndVmId.and("instanceId", ListByIpAndVmId.entity().getInstanceId(), SearchCriteria.Op.EQ); + ListByIpAndVmId.done(); + + ListVmIdByNetworkGroup = createSearchBuilder(Long.class); + ListVmIdByNetworkGroup.and("networkGroupId", ListVmIdByNetworkGroup.entity().getNetworkGroupId(), SearchCriteria.Op.EQ); + ListVmIdByNetworkGroup.selectField(ListVmIdByNetworkGroup.entity().getInstanceId()); + ListVmIdByNetworkGroup.done(); + + ListByNetworkGroup = createSearchBuilder(); + ListByNetworkGroup.and("networkGroupId", ListByNetworkGroup.entity().getNetworkGroupId(), SearchCriteria.Op.EQ); + ListByNetworkGroup.done(); + + ListByIp = createSearchBuilder(); + ListByIp.and("ipAddress", ListByIp.entity().getGuestIpAddress(), SearchCriteria.Op.EQ); + ListByIp.done(); + + ListByVmId = createSearchBuilder(); + ListByVmId.and("instanceId", ListByVmId.entity().getInstanceId(), SearchCriteria.Op.EQ); + ListByVmId.done(); + + ListByNetworkGroupAndStates = createSearchBuilder(); + ListByNetworkGroupAndStates.and("networkGroupId", ListByNetworkGroupAndStates.entity().getNetworkGroupId(), SearchCriteria.Op.EQ); + ListByNetworkGroupAndStates.and("states", ListByNetworkGroupAndStates.entity().getVmState(), SearchCriteria.Op.IN); + ListByNetworkGroupAndStates.done(); + + ListByVmIdGroupId = createSearchBuilder(); + ListByVmIdGroupId.and("instanceId", ListByVmIdGroupId.entity().getInstanceId(), SearchCriteria.Op.EQ); + ListByVmIdGroupId.and("networkGroupId", ListByVmIdGroupId.entity().getNetworkGroupId(), SearchCriteria.Op.EQ); + ListByVmIdGroupId.done(); + } + + public List listByIpAndInstanceId(String ipAddress, long vmId) { + SearchCriteria sc = ListByIpAndVmId.create(); + sc.setParameters("ipAddress", ipAddress); + sc.setParameters("instanceId", vmId); + return listActiveBy(sc); + } + + @Override + public List listByNetworkGroup(long networkGroupId) { + SearchCriteria sc = ListByNetworkGroup.create(); + sc.setParameters("networkGroupId", networkGroupId); + return listActiveBy(sc); + } + + @Override + public List listByIp(String ipAddress) { + SearchCriteria sc = ListByIp.create(); + sc.setParameters("ipAddress", ipAddress); + return listActiveBy(sc); + } + + @Override + public List listByInstanceId(long vmId) { + SearchCriteria sc = ListByVmId.create(); + sc.setParameters("instanceId", vmId); + return listActiveBy(sc); + } + + @Override + public int deleteVM(long instanceId) { + SearchCriteria sc = ListByVmId.create(); + sc.setParameters("instanceId", instanceId); + return super.delete(sc); + } + + @Override + public List listByNetworkGroup(long networkGroupId, State... vmStates) { + SearchCriteria sc = ListByNetworkGroupAndStates.create(); + sc.setParameters("networkGroupId", networkGroupId); + sc.setParameters("states", (Object[])vmStates); + return listActiveBy(sc); + } + + @Override + public List listVmIdsByNetworkGroup(long networkGroupId) { + SearchCriteria sc = ListVmIdByNetworkGroup.create(); + sc.setParameters("networkGroupId", networkGroupId); + return searchAll(sc, null); + } + + @Override + public NetworkGroupVMMapVO findByVmIdGroupId(long instanceId, long networkGroupId) { + SearchCriteria sc = ListByVmIdGroupId.create(); + sc.setParameters("networkGroupId", networkGroupId); + sc.setParameters("instanceId", instanceId); + return findOneBy(sc); + } + +} diff --git a/core/src/com/cloud/network/security/dao/NetworkGroupWorkDao.java b/core/src/com/cloud/network/security/dao/NetworkGroupWorkDao.java new file mode 100644 index 00000000000..8eda8a0a95b --- /dev/null +++ b/core/src/com/cloud/network/security/dao/NetworkGroupWorkDao.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.network.security.NetworkGroupWorkVO; +import com.cloud.network.security.NetworkGroupWorkVO.Step; +import com.cloud.utils.db.GenericDao; + +public interface NetworkGroupWorkDao extends GenericDao { + NetworkGroupWorkVO findByVmId(long vmId, boolean taken); + + NetworkGroupWorkVO findByVmIdStep(long vmId, Step step); + + + NetworkGroupWorkVO take(long serverId); + + void updateStep(Long vmId, Long logSequenceNumber, Step done); + + void updateStep(Long workId, Step done); + + int deleteFinishedWork(Date timeBefore); + + List findUnfinishedWork(Date timeBefore); + + +} diff --git a/core/src/com/cloud/network/security/dao/NetworkGroupWorkDaoImpl.java b/core/src/com/cloud/network/security/dao/NetworkGroupWorkDaoImpl.java new file mode 100644 index 00000000000..34f219d132a --- /dev/null +++ b/core/src/com/cloud/network/security/dao/NetworkGroupWorkDaoImpl.java @@ -0,0 +1,209 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security.dao; + +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.ha.WorkVO; +import com.cloud.network.security.NetworkGroupWorkVO; +import com.cloud.network.security.NetworkGroupWorkVO.Step; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.SearchCriteria.Op; +import com.cloud.utils.exception.CloudRuntimeException; + +@Local(value={NetworkGroupWorkDao.class}) +public class NetworkGroupWorkDaoImpl extends GenericDaoBase implements NetworkGroupWorkDao { + private SearchBuilder VmIdTakenSearch; + private SearchBuilder VmIdSeqNumSearch; + private SearchBuilder VmIdUnTakenSearch; + private SearchBuilder UntakenWorkSearch; + private SearchBuilder VmIdStepSearch; + private SearchBuilder CleanupSearch; + + + protected NetworkGroupWorkDaoImpl() { + VmIdTakenSearch = createSearchBuilder(); + VmIdTakenSearch.and("vmId", VmIdTakenSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + VmIdTakenSearch.and("taken", VmIdTakenSearch.entity().getDateTaken(), SearchCriteria.Op.NNULL); + + VmIdTakenSearch.done(); + + VmIdUnTakenSearch = createSearchBuilder(); + VmIdUnTakenSearch.and("vmId", VmIdUnTakenSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + VmIdUnTakenSearch.and("taken", VmIdUnTakenSearch.entity().getDateTaken(), SearchCriteria.Op.NULL); + + VmIdUnTakenSearch.done(); + + UntakenWorkSearch = createSearchBuilder(); + UntakenWorkSearch.and("server", UntakenWorkSearch.entity().getServerId(), SearchCriteria.Op.NULL); + UntakenWorkSearch.and("taken", UntakenWorkSearch.entity().getDateTaken(), SearchCriteria.Op.NULL); + UntakenWorkSearch.and("step", UntakenWorkSearch.entity().getStep(), SearchCriteria.Op.EQ); + + UntakenWorkSearch.done(); + + VmIdSeqNumSearch = createSearchBuilder(); + VmIdSeqNumSearch.and("vmId", VmIdSeqNumSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + VmIdSeqNumSearch.and("seqno", VmIdSeqNumSearch.entity().getLogsequenceNumber(), SearchCriteria.Op.EQ); + + VmIdSeqNumSearch.done(); + + VmIdStepSearch = createSearchBuilder(); + VmIdStepSearch.and("vmId", VmIdStepSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + VmIdStepSearch.and("step", VmIdStepSearch.entity().getStep(), SearchCriteria.Op.EQ); + + VmIdStepSearch.done(); + + CleanupSearch = createSearchBuilder(); + CleanupSearch.and("taken", CleanupSearch.entity().getDateTaken(), Op.LTEQ); + CleanupSearch.and("step", CleanupSearch.entity().getStep(), SearchCriteria.Op.IN); + + CleanupSearch.done(); + + + } + + @Override + public NetworkGroupWorkVO findByVmId(long vmId, boolean taken) { + SearchCriteria sc = taken?VmIdTakenSearch.create():VmIdUnTakenSearch.create(); + sc.setParameters("vmId", vmId); + return findOneBy(sc); + } + + @Override + public NetworkGroupWorkVO take(long serverId) { + final Transaction txn = Transaction.currentTxn(); + try { + final SearchCriteria sc = UntakenWorkSearch.create(); + sc.setParameters("step", Step.Scheduled); + + final Filter filter = new Filter(NetworkGroupWorkVO.class, null, true, 0l, 1l);//FIXME: order desc by update time? + + txn.start(); + final List vos = lock(sc, filter, true); + if (vos.size() == 0) { + txn.commit(); + return null; + } + NetworkGroupWorkVO work = null; + for (NetworkGroupWorkVO w: vos) { + //ensure that there is no job in Processing state for the same VM + if ( findByVmIdStep(w.getInstanceId(), Step.Processing) == null) { + work = w; + break; + } + } + if (work == null) { + txn.commit(); + return null; + } + work.setServerId(serverId); + work.setDateTaken(new Date()); + work.setStep(NetworkGroupWorkVO.Step.Processing); + + update(work.getId(), work); + + txn.commit(); + + return work; + + } catch (final Throwable e) { + throw new CloudRuntimeException("Unable to execute take", e); + } + } + + @Override + public void updateStep(Long vmId, Long logSequenceNumber, Step step) { + final Transaction txn = Transaction.currentTxn(); + txn.start(); + SearchCriteria sc = VmIdSeqNumSearch.create(); + sc.setParameters("vmId", vmId); + sc.setParameters("seqno", logSequenceNumber); + + final Filter filter = new Filter(WorkVO.class, null, true, 0l, 1l); + + final List vos = lock(sc, filter, true); + if (vos.size() == 0) { + txn.commit(); + return; + } + NetworkGroupWorkVO work = vos.get(0); + work.setStep(step); + update(work.getId(), work); + + txn.commit(); + } + + @Override + public NetworkGroupWorkVO findByVmIdStep(long vmId, Step step) { + SearchCriteria sc = VmIdStepSearch.create(); + sc.setParameters("vmId", vmId); + sc.setParameters("step", step); + return findOneBy(sc); + } + + @Override + public void updateStep(Long workId, Step step) { + final Transaction txn = Transaction.currentTxn(); + txn.start(); + + NetworkGroupWorkVO work = lock(workId, true); + if (work == null) { + txn.commit(); + return; + } + work.setStep(step); + update(work.getId(), work); + + txn.commit(); + + } + + @Override + public int deleteFinishedWork(Date timeBefore) { + final SearchCriteria sc = CleanupSearch.create(); + sc.setParameters("taken", timeBefore); + sc.setParameters("step", Step.Done); + + return delete(sc); + } + + @Override + public List findUnfinishedWork(Date timeBefore) { + final SearchCriteria sc = CleanupSearch.create(); + sc.setParameters("taken", timeBefore); + sc.setParameters("step", Step.Processing); + + List result = listBy(sc); + + NetworkGroupWorkVO work = createForUpdate(); + work.setStep(Step.Error); + update(work, sc); + + return result; + } + + +} diff --git a/core/src/com/cloud/network/security/dao/VmRulesetLogDao.java b/core/src/com/cloud/network/security/dao/VmRulesetLogDao.java new file mode 100644 index 00000000000..6991dcddf67 --- /dev/null +++ b/core/src/com/cloud/network/security/dao/VmRulesetLogDao.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security.dao; + +import com.cloud.network.security.VmRulesetLogVO; +import com.cloud.utils.db.GenericDao; + +public interface VmRulesetLogDao extends GenericDao { + VmRulesetLogVO findByVmId(long vmId); + +} diff --git a/core/src/com/cloud/network/security/dao/VmRulesetLogDaoImpl.java b/core/src/com/cloud/network/security/dao/VmRulesetLogDaoImpl.java new file mode 100644 index 00000000000..469012ed53b --- /dev/null +++ b/core/src/com/cloud/network/security/dao/VmRulesetLogDaoImpl.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.network.security.dao; + +import javax.ejb.Local; + +import com.cloud.network.security.VmRulesetLogVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={VmRulesetLogDao.class}) +public class VmRulesetLogDaoImpl extends GenericDaoBase implements VmRulesetLogDao { + private SearchBuilder VmIdSearch; + + + protected VmRulesetLogDaoImpl() { + VmIdSearch = createSearchBuilder(); + VmIdSearch.and("vmId", VmIdSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + + VmIdSearch.done(); + + } + + @Override + public VmRulesetLogVO findByVmId(long vmId) { + SearchCriteria sc = VmIdSearch.create(); + sc.setParameters("vmId", vmId); + return findOneBy(sc); + } + + +} diff --git a/core/src/com/cloud/offerings/NetworkOfferingVO.java b/core/src/com/cloud/offerings/NetworkOfferingVO.java new file mode 100644 index 00000000000..26ad13b8c13 --- /dev/null +++ b/core/src/com/cloud/offerings/NetworkOfferingVO.java @@ -0,0 +1,105 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.offerings; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.offering.NetworkOffering; + +@Entity +@Table(name="network_offerings") +public class NetworkOfferingVO implements NetworkOffering { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + long id; + + @Column(name="name") + String name; + + @Column(name="display_text") + String displayText; + + @Column(name="rate") + Integer rateMbps; + + @Column(name="multicast_rate") + Integer multicastRateMbps; + + @Column(name="concurrent_connections") + Integer concurrentConnections; + + @Column(name="type") + @Enumerated(value=EnumType.STRING) + GuestIpType guestIpType; + + @Override + public String getDisplayText() { + return displayText; + } + + @Override + public GuestIpType getGuestIpType() { + return guestIpType; + } + + @Override + public long getId() { + return id; + } + + @Override + public Integer getMulticastRateMbps() { + return multicastRateMbps; + } + + @Override + public String getName() { + return name; + } + + @Override + public Integer getRateMbps() { + return rateMbps; + } + + public NetworkOfferingVO() { + } + + public NetworkOfferingVO(String name, String displayText, GuestIpType type, Integer rateMbps, Integer multicastRateMbps, Integer concurrentConnections) { + this.name = name; + this.displayText = displayText; + this.guestIpType = type; + this.rateMbps = rateMbps; + this.multicastRateMbps = multicastRateMbps; + this.concurrentConnections = concurrentConnections; + } + + @Override + public Integer getConcurrentConnections() { + return concurrentConnections; + } + +} diff --git a/core/src/com/cloud/resource/DiskPreparer.java b/core/src/com/cloud/resource/DiskPreparer.java new file mode 100644 index 00000000000..c24813d66d9 --- /dev/null +++ b/core/src/com/cloud/resource/DiskPreparer.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.resource; + +import com.cloud.storage.VolumeVO; +import com.cloud.storage.VirtualMachineTemplate.BootloaderType; +import com.cloud.utils.component.Adapter; + +/** + * DiskMounter mounts and unmounts disk for VMs + * to consume. + * + */ +public interface DiskPreparer extends Adapter { + /** + * Mounts a volumeVO and returns a path. + * + * @param vol + * @return + */ + public String mount(String vmName, VolumeVO vol, BootloaderType type); + + /** + * Unmounts + */ + public boolean unmount(String path); + +} diff --git a/core/src/com/cloud/resource/NetworkPreparer.java b/core/src/com/cloud/resource/NetworkPreparer.java new file mode 100644 index 00000000000..998f386b143 --- /dev/null +++ b/core/src/com/cloud/resource/NetworkPreparer.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.resource; + +import com.cloud.utils.component.Adapter; + +/** + * Prepares the network for VM. + */ +public interface NetworkPreparer extends Adapter { + + String setup(String vnet); + + void cleanup(String vnet); +} diff --git a/core/src/com/cloud/resource/ServerResource.java b/core/src/com/cloud/resource/ServerResource.java new file mode 100755 index 00000000000..b056a478016 --- /dev/null +++ b/core/src/com/cloud/resource/ServerResource.java @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.resource; + +import com.cloud.agent.IAgentControl; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.host.Host; +import com.cloud.utils.component.Manager; + +/** + * + * ServerResource is a generic container to execute commands sent + * to the agent. + */ +public interface ServerResource extends Manager { + /** + * @return Host.Type type of the computing server we have. + */ + Host.Type getType(); + + /** + * Generate a startup command containing information regarding the resource. + * @return StartupCommand ready to be sent to the management server. + */ + public StartupCommand[] initialize(); + + /** + * @param id id of the server to put in the PingCommand + * @return PingCommand + */ + public PingCommand getCurrentStatus(long id); + + /** + * Execute the request coming from the computing server. + * @param cmd Command to execute. + * @return Answer + */ + public Answer executeRequest(Command cmd); + +// public void revertRequest(Command cmd); + + /** + * disconnected() is called when the connection is down between the + * agent and the management server. If there are any cleanups, this + * is the time to do it. + */ + public void disconnected(); + + /** + * This is added to allow calling agent control service from within the resource + * @return + */ + public IAgentControl getAgentControl(); + + public void setAgentControl(IAgentControl agentControl); +} diff --git a/core/src/com/cloud/resource/ServerResourceBase.java b/core/src/com/cloud/resource/ServerResourceBase.java new file mode 100755 index 00000000000..c10da164984 --- /dev/null +++ b/core/src/com/cloud/resource/ServerResourceBase.java @@ -0,0 +1,304 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.resource; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.agent.IAgentControl; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.StartupCommand; +import com.cloud.utils.net.NetUtils; +import com.cloud.utils.script.Script; + +public abstract class ServerResourceBase implements ServerResource { + private static final Logger s_logger = Logger.getLogger(ServerResourceBase.class); + protected String _name; + private ArrayList _warnings = new ArrayList(); + private ArrayList _errors = new ArrayList(); + protected NetworkInterface _publicNic; + protected NetworkInterface _privateNic; + protected NetworkInterface _storageNic; + protected NetworkInterface _storageNic2; + protected IAgentControl _agentControl; + + @Override + public String getName() { + return _name; + } + + protected String findScript(String script) { + return Script.findScript(getDefaultScriptsDir(), script); + } + + protected abstract String getDefaultScriptsDir(); + + @Override + public boolean configure(final String name, final Map params) throws ConfigurationException { + _name = name; + + String publicNic = (String)params.get("public.network.device"); + if (publicNic == null) { + publicNic = "xenbr1"; + } + String privateNic = (String)params.get("private.network.device"); + if (privateNic == null) { + privateNic = "xenbr0"; + } + final String storageNic = (String)params.get("storage.network.device"); + final String storageNic2 = (String)params.get("storage.network.device.2"); + + _privateNic = getNetworkInterface(privateNic); + _publicNic = getNetworkInterface(publicNic); + _storageNic = getNetworkInterface(storageNic); + _storageNic2 = getNetworkInterface(storageNic2); + + if (_privateNic == null) { + s_logger.error("Nics are not configured!"); + + Enumeration nics = null; + try { + nics = NetworkInterface.getNetworkInterfaces(); + if (nics == null || !nics.hasMoreElements()) { + throw new ConfigurationException("Private NIC is not configured"); + } + } catch (final SocketException e) { + throw new ConfigurationException("Private NIC is not configured"); + } + + while (nics.hasMoreElements()) { + final NetworkInterface nic = nics.nextElement(); + final String nicName = nic.getName(); + // try { + if (//!nic.isLoopback() && + //nic.isUp() && + !nic.isVirtual() && + !nicName.startsWith("vnif") && + !nicName.startsWith("vnbr") && + !nicName.startsWith("peth") && + !nicName.startsWith("vif") && + !nicName.startsWith("virbr") && + !nicName.contains(":")) { + final String[] info = NetUtils.getNicParams(nicName); + if (info != null && info[0] != null) { + _privateNic = nic; + s_logger.info("Designating private to be nic " + nicName); + break; + } + } + // } catch (final SocketException e) { + // s_logger.warn("Error looking at " + nicName, e); + // } + s_logger.debug("Skipping nic " + nicName); + } + + if (_privateNic == null) { + throw new ConfigurationException("Private NIC is not configured"); + } + } + + return true; + } + + protected NetworkInterface getNetworkInterface(String nicName) { + s_logger.debug("Retrieving network interface: " + nicName); + if (nicName == null) { + return null; + } + + if (nicName.trim().length() == 0) { + return null; + } + + nicName = nicName.trim(); + + NetworkInterface nic; + try { + nic = NetworkInterface.getByName(nicName); + if (nic == null) { + s_logger.debug("Unable to get network interface for " + nicName); + return null; + } + + return nic; + } catch (final SocketException e) { + s_logger.warn("Unable to get network interface for " + nicName, e); + return null; + } + } + + protected void fillNetworkInformation(final StartupCommand cmd) { + String[] info = null; + if (_privateNic != null) { + info = NetUtils.getNetworkParams(_privateNic); + if (info != null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Parameters for private nic: " + info[0] + " - " + info[1] + "-" + info[2]); + } + cmd.setPrivateIpAddress(info[0]); + cmd.setPrivateMacAddress(info[1]); + cmd.setPrivateNetmask(info[2]); + } + } + + if (_storageNic != null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Storage has its now nic: " + _storageNic.getName()); + } + info = NetUtils.getNetworkParams(_storageNic); + } + + // NOTE: In case you're wondering, this is not here by mistake. + if (info != null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Parameters for storage nic: " + info[0] + " - " + info[1] + "-" + info[2]); + } + cmd.setStorageIpAddress(info[0]); + cmd.setStorageMacAddress(info[1]); + cmd.setStorageNetmask(info[2]); + } + + if (_publicNic != null) { + info = NetUtils.getNetworkParams(_publicNic); + if (info != null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Parameters for pubic nic: " + info[0] + " - " + info[1] + "-" + info[2]); + } + cmd.setPublicIpAddress(info[0]); + cmd.setPublicMacAddress(info[1]); + cmd.setPublicNetmask(info[2]); + } + } + + if (_storageNic2 != null) { + info = NetUtils.getNetworkParams(_storageNic2); + if (info != null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Parameters for storage nic 2: " + info[0] + " - " + info[1] + "-" + info[2]); + } + cmd.setStorageIpAddressDeux(info[0]); + cmd.setStorageMacAddressDeux(info[1]); + cmd.setStorageNetmaskDeux(info[2]); + } + } + } + + @Override + public void disconnected() { + } + + @Override + public IAgentControl getAgentControl() { + return _agentControl; + } + + @Override + public void setAgentControl(IAgentControl agentControl) { + _agentControl = agentControl; + } + + protected void recordWarning(final String msg, final Throwable th) { + final String str = getLogStr(msg, th); + synchronized(_warnings) { + _warnings.add(str); + } + } + + protected void recordWarning(final String msg) { + recordWarning(msg, null); + } + + protected List getWarnings() { + synchronized(this) { + final ArrayList results = _warnings; + _warnings = new ArrayList(); + return results; + } + } + + protected List getErrors() { + synchronized(this) { + final ArrayList result = _errors; + _errors = new ArrayList(); + return result; + } + } + + protected void recordError(final String msg, final Throwable th) { + final String str = getLogStr(msg, th); + synchronized(_errors) { + _errors.add(str); + } + } + + protected void recordError(final String msg) { + recordError(msg, null); + } + + protected Answer createErrorAnswer(final Command cmd, final String msg, final Throwable th) { + final StringWriter writer = new StringWriter(); + if (msg != null) { + writer.append(msg); + } + writer.append("===>Stack<==="); + th.printStackTrace(new PrintWriter(writer)); + return new Answer(cmd, false, writer.toString()); + } + + protected String createErrorDetail(final String msg, final Throwable th) { + final StringWriter writer = new StringWriter(); + if (msg != null) { + writer.append(msg); + } + writer.append("===>Stack<==="); + th.printStackTrace(new PrintWriter(writer)); + return writer.toString(); + } + + protected String getLogStr(final String msg, final Throwable th) { + final StringWriter writer = new StringWriter(); + writer.append(new Date().toString()).append(": ").append(msg); + if (th != null) { + writer.append("\n Exception: "); + th.printStackTrace(new PrintWriter(writer)); + } + return writer.toString(); + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } +} diff --git a/core/src/com/cloud/serializer/GsonHelper.java b/core/src/com/cloud/serializer/GsonHelper.java new file mode 100644 index 00000000000..b241a15eedb --- /dev/null +++ b/core/src/com/cloud/serializer/GsonHelper.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.serializer; + +import java.lang.reflect.Type; +import java.util.List; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.transport.ArrayTypeAdaptor; +import com.cloud.agent.transport.VolListTypeAdaptor; +import com.cloud.storage.VolumeVO; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +public class GsonHelper { + private static final GsonBuilder s_gBuilder; + static { + s_gBuilder = new GsonBuilder(); + s_gBuilder.setVersion(1.3); + s_gBuilder.registerTypeAdapter(Command[].class, new ArrayTypeAdaptor()); + s_gBuilder.registerTypeAdapter(Answer[].class, new ArrayTypeAdaptor()); + Type listType = new TypeToken>() {}.getType(); + s_gBuilder.registerTypeAdapter(listType, new VolListTypeAdaptor()); + } + + public static GsonBuilder getBuilder() { + return s_gBuilder; + } +} diff --git a/core/src/com/cloud/serializer/Param.java b/core/src/com/cloud/serializer/Param.java new file mode 100644 index 00000000000..8072b2d3779 --- /dev/null +++ b/core/src/com/cloud/serializer/Param.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.serializer; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Param { + String name() default ""; + String propName() default ""; +} diff --git a/core/src/com/cloud/serializer/SerializerHelper.java b/core/src/com/cloud/serializer/SerializerHelper.java new file mode 100644 index 00000000000..375db652f2f --- /dev/null +++ b/core/src/com/cloud/serializer/SerializerHelper.java @@ -0,0 +1,172 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.serializer; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.apache.log4j.Logger; + +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.google.gson.Gson; + +/** + * Note: toPairList and appendPairList only support simple POJO objects currently + */ +public class SerializerHelper { + public static final Logger s_logger = Logger.getLogger(SerializerHelper.class.getName()); + + public static String toSerializedString(Object result) { + if(result != null) { + Class clz = result.getClass(); + Gson gson = GsonHelper.getBuilder().create(); + + return clz.getName() + "/" + gson.toJson(result); + } + return null; + } + + public static Object fromSerializedString(String result) { + try { + if(result != null && !result.isEmpty()) { + int seperatorPos = result.indexOf('/'); + if(seperatorPos < 0) + return null; + + String clzName = result.substring(0, seperatorPos); + String content = result.substring(seperatorPos + 1); + Class clz; + try { + clz = Class.forName(clzName); + } catch (ClassNotFoundException e) { + return null; + } + + Gson gson = GsonHelper.getBuilder().create(); + return gson.fromJson(content, clz); + } + return null; + } catch(RuntimeException e) { + s_logger.error("Caught runtime exception when doing GSON descrialization on: " + result); + throw e; + } + } + + public static List> toPairList(Object o, String name) { + List> l = new ArrayList>(); + return appendPairList(l, o, name); + } + + public static List> appendPairList(List> l, Object o, String name) { + if(o != null) { + Class clz = o.getClass(); + + if(clz.isPrimitive() || clz.getSuperclass() == Number.class || clz == String.class || clz == Date.class) { + l.add(new Pair(name, o.toString())); + return l; + } + + for(Field f : clz.getDeclaredFields()) { + if((f.getModifiers() & Modifier.STATIC) != 0) + continue; + + Param param = f.getAnnotation(Param.class); + if(param == null) + continue; + + String propName = f.getName(); + if(!param.propName().isEmpty()) + propName = param.propName(); + + String paramName = param.name(); + if(paramName.isEmpty()) + paramName = propName; + + Method method = getGetMethod(o, propName); + if(method != null) { + try { + Object fieldValue = method.invoke(o); + if(fieldValue != null) { + if (f.getType() == Date.class) { + l.add(new Pair(paramName, DateUtil.getOutputString((Date)fieldValue))); + } else { + l.add(new Pair(paramName, fieldValue.toString())); + } + } + //else + // l.add(new Pair(paramName, "")); + } catch (IllegalArgumentException e) { + s_logger.error("Illegal argument exception when calling POJO " + o.getClass().getName() + " get method for property: " + propName); + + } catch (IllegalAccessException e) { + s_logger.error("Illegal access exception when calling POJO " + o.getClass().getName() + " get method for property: " + propName); + } catch (InvocationTargetException e) { + s_logger.error("Invocation target exception when calling POJO " + o.getClass().getName() + " get method for property: " + propName); + } + } + } + } + return l; + } + + private static Method getGetMethod(Object o, String propName) { + Method method = null; + String methodName = getGetMethodName("get", propName); + try { + method = o.getClass().getMethod(methodName); + } catch (SecurityException e1) { + s_logger.error("Security exception in getting POJO " + o.getClass().getName() + " get method for property: " + propName); + } catch (NoSuchMethodException e1) { + if(s_logger.isTraceEnabled()) + s_logger.trace("POJO " + o.getClass().getName() + " does not have " + methodName + "() method for property: " + propName + ", will check is-prefixed method to see if it is boolean property"); + } + + if(method != null) + return method; + + methodName = getGetMethodName("is", propName); + try { + method = o.getClass().getMethod(methodName); + } catch (SecurityException e1) { + s_logger.error("Security exception in getting POJO " + o.getClass().getName() + " get method for property: " + propName); + } catch (NoSuchMethodException e1) { + s_logger.warn("POJO " + o.getClass().getName() + " does not have " + methodName + "() method for property: " + propName); + } + return method; + } + + private static String getGetMethodName(String prefix, String fieldName) { + StringBuffer sb = new StringBuffer(prefix); + + if(fieldName.length() >= prefix.length() && fieldName.substring(0, prefix.length()).equals(prefix)) { + return fieldName; + } else { + sb.append(fieldName.substring(0, 1).toUpperCase()); + sb.append(fieldName.substring(1)); + } + + return sb.toString(); + } +} diff --git a/core/src/com/cloud/server/Criteria.java b/core/src/com/cloud/server/Criteria.java new file mode 100644 index 00000000000..f2d7090341c --- /dev/null +++ b/core/src/com/cloud/server/Criteria.java @@ -0,0 +1,134 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.server; + +import java.util.HashMap; + +public class Criteria { + + private Long offset; + private Long limit; + private String orderBy; + private Boolean ascending; + private final HashMap criteria; + + public static final String ID = "id"; + public static final String USERID = "userId"; + public static final String NAME = "name"; + public static final String NOTSTATE = "notState"; + public static final String STATE = "state"; + public static final String DATACENTERID = "dataCenterId"; + public static final String DESCRIPTION = "description"; + public static final String PODID = "podId"; + public static final String CLUSTERID = "clusterId"; + public static final String HOSTID = "hostId"; + public static final String OSCATEGORYID = "osCategoryId"; + public static final String PODNAME = "podName"; + public static final String ZONENAME = "zoneName"; + public static final String HOSTNAME = "hostName"; + public static final String HOST = "host"; + public static final String USERNAME = "username"; + public static final String TYPE = "type"; + public static final String STATUS = "status"; + public static final String READY = "ready"; + public static final String ISPUBLIC = "isPublic"; + public static final String ADDRESS = "address"; + public static final String REMOVED = "removed"; + public static final String ISRECURSIVE = "isRecursive"; + public static final String ISDISABLED = "isDisabled"; + public static final String ISCLEANUPREQUIRED = "isCleanupRequired"; + public static final String LEVEL = "level"; + public static final String STARTDATE = "startDate"; + public static final String ENDDATE = "endDate"; + public static final String VTYPE = "vType"; + public static final String INSTANCEID = "instanceId"; + public static final String VOLUMEID = "volumeId"; + public static final String DOMAINID = "domainId"; + public static final String DOMAIN = "domain"; + public static final String ACCOUNTID = "accountId"; + public static final String ACCOUNTNAME = "accountName"; + public static final String CATEGORY = "category"; + public static final String CREATED_BY = "createdBy"; + public static final String GROUPID = "groupId"; + public static final String PATH = "path"; + public static final String KEYWORD = "keyword"; + public static final String ISADMIN = "isadmin"; + public static final String VLAN = "vlan"; + public static final String ISALLOCATED = "isallocated"; + public static final String IPADDRESS = "ipaddress"; + public static final String FOR_VIRTUAL_NETWORK = "forvirtualnetwork"; + public static final String TARGET_IQN = "targetiqn"; + public static final String SCOPE = "scope"; + public static final String NETWORKGROUP = "networkGroup"; + + + public Criteria(String orderBy, Boolean ascending, Long offset, Long limit) { + this.offset = offset; + this.limit = limit; + this.orderBy = orderBy; + this.ascending = ascending; + criteria = new HashMap(); + } + + public Criteria() { + criteria = new HashMap(); + this.ascending = false; + } + + public Long getOffset() { + return offset; + } + + public void addCriteria(String name, Object val) { + criteria.put(name, val); + } + + public Object getCriteria(String name) { + return criteria.get(name); + } + + public void setOffset(Long offset) { + this.offset = offset; + } + + public Long getLimit() { + return limit; + } + + public void setLimit(Long limit) { + this.limit = limit; + } + + public String getOrderBy() { + return orderBy; + } + + public void setOrderBy(String orderBy) { + this.orderBy = orderBy; + } + + public Boolean getAscending() { + return ascending; + } + + public void setAscending(Boolean ascending) { + this.ascending = ascending; + } + +} diff --git a/core/src/com/cloud/server/ManagementServer.java b/core/src/com/cloud/server/ManagementServer.java new file mode 100644 index 00000000000..a78e68ef140 --- /dev/null +++ b/core/src/com/cloud/server/ManagementServer.java @@ -0,0 +1,2187 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General License for more details. + * + * You should have received a copy of the GNU General License + * along with this program. If not, see . + * + */ +package com.cloud.server; + +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import com.cloud.alert.AlertVO; +import com.cloud.async.AsyncJobResult; +import com.cloud.async.AsyncJobVO; +import com.cloud.capacity.CapacityVO; +import com.cloud.configuration.ConfigurationVO; +import com.cloud.configuration.ResourceLimitVO; +import com.cloud.configuration.ResourceCount.ResourceType; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.DataCenterIpAddressVO; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.HostPodVO; +import com.cloud.dc.VlanVO; +import com.cloud.dc.Vlan.VlanType; +import com.cloud.domain.DomainVO; +import com.cloud.event.EventVO; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.DiscoveryException; +import com.cloud.exception.InsufficientAddressCapacityException; +import com.cloud.exception.InsufficientStorageCapacityException; +import com.cloud.exception.InternalErrorException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceInUseException; +import com.cloud.exception.StorageUnavailableException; +import com.cloud.host.Host; +import com.cloud.host.HostStats; +import com.cloud.host.HostVO; +import com.cloud.info.ConsoleProxyInfo; +import com.cloud.network.FirewallRuleVO; +import com.cloud.network.IPAddressVO; +import com.cloud.network.LoadBalancerVO; +import com.cloud.network.NetworkRuleConfigVO; +import com.cloud.network.SecurityGroupVO; +import com.cloud.network.security.IngressRuleVO; +import com.cloud.network.security.NetworkGroupRulesVO; +import com.cloud.network.security.NetworkGroupVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.DiskTemplateVO; +import com.cloud.storage.GuestOS; +import com.cloud.storage.GuestOSCategoryVO; +import com.cloud.storage.GuestOSVO; +import com.cloud.storage.Snapshot; +import com.cloud.storage.SnapshotPolicyVO; +import com.cloud.storage.SnapshotScheduleVO; +import com.cloud.storage.SnapshotVO; +import com.cloud.storage.StoragePoolVO; +import com.cloud.storage.StorageStats; +import com.cloud.storage.VMTemplateHostVO; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VolumeStats; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VMTemplateDao.TemplateFilter; +import com.cloud.storage.preallocatedlun.PreallocatedLunVO; +import com.cloud.user.Account; +import com.cloud.user.AccountVO; +import com.cloud.user.User; +import com.cloud.user.UserAccount; +import com.cloud.user.UserAccountVO; +import com.cloud.user.UserStatisticsVO; +import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.ExecutionException; +import com.cloud.vm.ConsoleProxyVO; +import com.cloud.vm.DomainRouter; +import com.cloud.vm.DomainRouterVO; +import com.cloud.vm.SecondaryStorageVmVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VmStats; + +/** + * ManagementServer is the interface to talk to the Managment Server. + * This will be the line drawn between the UI and MS. If we need to build + * a wire protocol, it will be built on top of this java interface. + */ +public interface ManagementServer { + static final String Name = "management-server"; + + /** + * Creates a new user, encrypts the password on behalf of the caller + * + * @param username username + * @param password the user's password + * @param firstName the user's first name + * @param lastName the user's last name + * @param domain the id of the domain that this user belongs to + * @param accountName the name(a.k.a. id) of the account that this user belongs to + * @param timezone the user's current timezone (default: PST) + * @return a user object + */ + User createUser(String username, String password, String firstName, String lastName, Long domain, String accountName, short userType, String email, String timezone); + boolean reconnect(long hostId); + long reconnectAsync(long hostId); + + ClusterVO findClusterById(long clusterId); + List listClusterByPodId(long podId); + + ClusterVO createCluster(long dcId, long podId, String name); + + /** + * Creates a new user, does not encrypt the password + * + * @param username username + * @param password the user's password + * @param firstName the user's first name + * @param lastName the user's last name + * @param domain the id of the domain that this user belongs to FIXME: if we have account, do we also need domain? + * @param accountName the name(a.k.a. id) of the account that this user belongs to + * @param timezone the user's current timezone (default: PST) + * @return a user object + */ + User createUserAPI(String username, String password, String firstName, String lastName, Long domain, String accountName, short userType, String email, String timezone); + + /** + * Gets a user by userId + * + * @param userId + * @return a user object + */ + User getUser(long userId); + + /** + * Gets a user and account by username and domain + * + * @param username + * @param domainId + * @return a user object + */ + UserAccount getUserAccount(String username, Long domainId); + + /** + * Gets a user and account by username, password and domain + * + * @param username + * @param password + * @param domainId + * @return a user object + */ + // UserAccount getUserAccount(String username, String password, Long domainId); + + /** + * Authenticates a user when s/he logs in. + * @param username required username for authentication + * @param password password to use for authentication, can be null for single sign-on case + * @param domainId id of domain where user with username resides + * @param requestParameters the request parameters of the login request, which should contain timestamp of when the request signature is made, and the signature itself in the single sign-on case + * @return a user object, null if the user failed to authenticate + */ + UserAccount authenticateUser(String username, String password, Long domainId, Map requestParameters); + + /** + * Deletes a user by userId + * @param userId + * @return true if delete was successful, false otherwise + */ + boolean deleteUser(long userId); + long deleteUserAsync(long userId); + + /** + * Disables a user by userId + * @param userId + * @return true if disable was successful, false otherwise + */ + boolean disableUser(long userId); + long disableUserAsync(long userId); + + /** + * Disables an account by accountId + * @param accountId + * @return true if disable was successful, false otherwise + */ + boolean disableAccount(long accountId); + long disableAccountAsync(long accountId); + + /** + * Enables an account by accountId + * @param accountId + * @return true if enable was successful, false otherwise + */ + boolean enableAccount(long accountId); + + /** + * Locks an account by accountId. A locked account cannot access the API, but will still have running VMs/IP addresses allocated/etc. + * @param accountId + * @return true if enable was successful, false otherwise + */ + boolean lockAccount(long accountId); + + /** + * Updates an account name by accountId + * @param accountId + * @param accountName + * @return true if update was successful, false otherwise + */ + + boolean updateAccount(long accountId, String accountName); + + /** + * Enables a user by userId + * @param userId + * @return true if enable was successful, false otherwise + */ + boolean enableUser(long userId); + + /** + * Locks a user by userId. A locked user cannot access the API, but will still have running VMs/IP addresses allocated/etc. + * @param userId + * @return true if enable was successful, false otherwise + */ + boolean lockUser(long userId); + + /** + * registerPreallocatedLun registers a preallocated lun in our database. + * + * @param targetIqn iqn for the storage server. + * @param portal portal ip address for the storage server. + * @param lun lun # + * @param size size of the lun + * @param dcId data center to attach to + * @param tags tags to attach to the lun + * @return the new PreAllocatedLun + */ + PreallocatedLunVO registerPreallocatedLun(String targetIqn, String portal, int lun, long size, long dcId, String tags); + + /** + * Unregisters a preallocated lun in our database + * @param id id of the lun + * @return true if unregistered; false if not. + * @throws IllegalArgumentException + */ + boolean unregisterPreallocatedLun(long id) throws IllegalArgumentException; + + + /** + * Discovers new hosts given an url to locate the resource. + * @param dcId id of the data center + * @param podid id of the pod + * @param clusterId id of the cluster + * @param url url to use + * @param username username to use to login + * @param password password to use to login + * @return true if hosts were found; false if not. + * @throws IllegalArgumentException + */ + List discoverHosts(long dcId, Long podId, Long clusterId, String url, String username, String password) throws IllegalArgumentException, DiscoveryException; + + String updateAdminPassword(long userId, String oldPassword, String newPassword); + + /** + * Updates a user's information + * @param userId + * @param username + * @param password + * @param firstname + * @param lastname + * @param email + * @param timezone + * @param apikey + * @param secretkey + * @return true if update was successful, false otherwise + * @throws InvalidParameterValueException + */ + boolean updateUser(long userId, String username, String password, String firstname, String lastname, String email, String timezone, String apiKey, String secretKey) throws InvalidParameterValueException; + + /** + * Locate a user by their apiKey + * @param apiKey that was created for a particular user + * @return the user/account pair if one exact match was found, null otherwise + */ + Pair findUserByApiKey(String apiKey); + + /** + * Get an account by the accountId + * @param accountId + * @return the account, or null if not found + */ + Account getAccount(long accountId); + + /** + * Create an API key for a user; this key is used as the user's identity when making + * calls to the developer API + * @param userId + * @return the new API key + */ + String createApiKey(Long userId); + + /** + * Create a secret key for a user, this key is used to sign requests made by the user + * to the developer API. When a request is received from a user, the secret key is + * retrieved from the database (using the apiKey) and used to verify the request signature. + * @param userId + * @return the new secret key + */ + String createSecretKey(Long userId); + + /** + * Gets Storage statistics for a given host + * + * @param hostId + * @return StorageStats + */ + StorageStats getStorageStatistics(long hostId); + + /** + * Gets the guest OS category for a host + * @param hostId + * @return guest OS Category + */ + GuestOSCategoryVO getHostGuestOSCategory(long hostId); + + + /** Get storage statistics (used/available) for a pool + * @param id pool id + * @return storage statistics + */ + StorageStats getStoragePoolStatistics(long id); + + /** + * prepares a host for maintenance. This method can take a long time + * depending on if there are any current operations on the host. + * + * @param hostId id of the host to bring down. + * @return true if the operation succeeds. + */ + boolean prepareForMaintenance(long hostId); + long prepareForMaintenanceAsync(long hostId) throws InvalidParameterValueException; + + /** + * prepares a primary storage for maintenance. + * + * @param primaryStorageId id of the storage to bring down. + * @return true if the operation succeeds. + */ + boolean preparePrimaryStorageForMaintenance(long primaryStorageId, long userId); + long preparePrimaryStorageForMaintenanceAsync(long primaryStorageId) throws InvalidParameterValueException; + + /** + * cancels primary storage from maintenance. + * + * @param primaryStorageId id of the storage to bring up. + * @return true if the operation succeeds. + */ + boolean cancelPrimaryStorageMaintenance(long primaryStorageId, long userId); + long cancelPrimaryStorageMaintenanceAsync(long primaryStorageId) throws InvalidParameterValueException; + + + /** + * Marks the host as maintenance completed. This actually will mark + * the host as down and the state will be changed automatically once + * the agent is up and running. + * + * @param hostId + * @return true if the state changed worked. false if not. + */ + boolean maintenanceCompleted(long hostId); + long maintenanceCompletedAsync(long hostId); + + /** + * Gets Host/VM statistics for a given host + * + * @param hostId + * @return HostStats/VMStats depending on the id passed + */ + VmStats getVmStatistics(long hostId); + + /** + * Gets Volume statistics. The array returned will contain VolumeStats in the same order + * as the array of volumes requested. + * + * @param volId + * @return array of VolumeStats + */ + VolumeStats[] getVolumeStatistics(long[] volId); + + /** + * Associate / allocate an IP address to a user + * @param userId + * @param accountId + * @param domainId + * @param zoneId + * @return allocated IP address in the zone specified + * @throws InsufficientAddressCapacityException if no more addresses are available + * @throws InvalidParameterValueException if no router for that user exists in the zone specified + * @throws InternalErrorException if the new address could not be sent down to the router + */ + String associateIpAddress(long userId, long accountId, long domainId, long zoneId) throws ResourceAllocationException, InsufficientAddressCapacityException, InvalidParameterValueException, InternalErrorException; + long associateIpAddressAsync(long userId, long accountId, long domainId, long zoneId); + + + /** + * Disassociate /unallocate an allocated IP address from a user + * @param userId + * @param accountId + * @param ipAddress + * @return success + */ + boolean disassociateIpAddress(long userId, long accountId, String ipAddress) throws PermissionDeniedException; + long disassociateIpAddressAsync(long userId, long accountId, String ipAddress); + + /** + * Adds a VLAN to the database, along with an IP address range. Can add three types of VLANs: (1) zone-wide VLANs on the virtual network (2) pod-wide direct attached VLANs (3) account-specific direct attached VLANs + * @param userId + * @param vlanType - either "DomR" (VLAN for a virtual network) or "DirectAttached" (VLAN for IPs that will be directly attached to UserVMs) + * @param zoneId + * @param accountId + * @param podId + * @param add + * @param vlanId + * @param gateway + * @param startIP + * @param endIP + * @return The new VlanVO object + * @throws Exception + */ + VlanVO createVlanAndPublicIpRange(long userId, VlanType vlanType, Long zoneId, Long accountId, Long podId, String vlanId, String vlanGateway, String vlanNetmask, String startIP, String endIP) throws Exception; + + /** + * Deletes a VLAN from the database, along with all of its IP addresses. Will not delete VLANs that have allocated IP addresses. + * @param userId + * @param vlanDbId + * @return success/failure + */ + boolean deleteVlanAndPublicIpRange(long userId, long vlanDbId) throws InvalidParameterValueException; + + /** + * Searches for vlan by the specified search criteria + * Can search by: "id", "vlan", "name", "zoneID" + * @param c + * @return List of Vlans + */ + List searchForVlans(Criteria c); + + /** + * If the specified VLAN is associated with the pod, returns the pod ID. Else, returns null. + * @param vlanDbId + * @return pod ID, or null + */ + Long getPodIdForVlan(long vlanDbId); + + /** + * If the specified VLAN is associated with a specific account, returns the account ID. Else, returns null. + * @param accountId + * @return account ID, or null + */ + Long getAccountIdForVlan(long vlanDbId); + + /** + * Creates a data volume + * @param accountId + * @pparam userId + * @param name - name for the volume + * @param zoneId - id of the zone to create this volume on + * @param diskOfferingId - id of the disk offering to create this volume with + * @param size - size of the volume + * @return true if success, false if not + */ + VolumeVO createVolume(long accountId, long userId, String name, long zoneId, long diskOfferingId, long startEventId, long size) throws InternalErrorException; + long createVolumeAsync(long accountId, long userId, String name, long zoneId, long diskOfferingId, long size) throws InvalidParameterValueException, InternalErrorException, ResourceAllocationException; + + /** + * Finds the root volume of the VM + * @param vmId + * @return Volume + */ + VolumeVO findRootVolume(long vmId); + + /** + * Marks a data volume as destroyed + * @param volumeId + */ + void destroyVolume(long volumeId) throws InvalidParameterValueException; + + /** + * Return a list of IP addresses + * @param accountId + * @param allocatedOnly - if true, will only list IPs that are allocated to the specified account + * @param zoneId - if specified, will list IPs in this zone + * @param vlanDbId - if specified, will list IPs in this VLAN + * @return list of IP addresses + */ + List listPublicIpAddressesBy(Long accountId, boolean allocatedOnly, Long zoneId, Long vlanDbId); + + /** + * Return a list of private IP addresses that have been allocated to the given pod and zone + * @param podId + * @param zoneId + * @return list of private IP addresses + */ + List listPrivateIpAddressesBy(Long podId, Long zoneId); + + /** + * Create or update a port forwarding rule + * @param userId userId calling this api + * @param accountId accountId calling this api + * @param publicIp Ip address + * @param publicPort port + * @param privateIp private address (10.x.y.z) to be forwarded to + * @param privatePort private port to be forwarded to + * @param proto protocol (tcp/udp/icmp) + * @return -1 if update/create failed, otherwise update/create succeeded + * @throws PermissionDeniedException if user is not authorized to operate on the supplied IP address + * @throws NetworkRuleConflictException if the new rule conflicts with existing rules + * @throws InvalidParameterValueException if the supplied parameters have invalid values + * @throws InternalErrorException if the update could not be performed + */ + // long createOrUpdateIpForwardingRule(long userId, long accountId, String publicIp, String publicPort, String privateIp, String privatePort, String proto) throws PermissionDeniedException, NetworkRuleConflictException, InvalidParameterValueException, InternalErrorException; + + /** + * Create or update a load balancing rule + * @param userId userId calling this api + * @param accountId accountId calling this api + * @param publicIp Ip address + * @param publicPort port + * @param privateIp private address (10.x.y.z) to be forwarded to + * @param privatePort private port to be forwarded to + * @param algo the load balancing algorithm + * @return -1 if update/create failed, otherwise update/create succeeded + * @throws PermissionDeniedException if user is not authorized to operate on the supplied IP address + * @throws NetworkRuleConflictException if the new rule conflicts with existing rules + * @throws InvalidParameterValueException if the supplied parameters have invalid values + * @throws InternalErrorException if the update could not be performed + */ + // long createOrUpdateLoadBalancerRule(long userId, long accountId, String publicIp, String publicPort, String privateIp, String privatePort, String algo) throws PermissionDeniedException, NetworkRuleConflictException, InvalidParameterValueException, InternalErrorException; + + /** + * Delete a ip forwarding rule + * @param userId userId calling this api + * @param accountId accountId calling this api + * @param publicIp Ip address + * @param publicPort port + * @param privateIp private address (10.x.y.z) to be forwarded to + * @param privatePort private port to be forwarded to + * @param proto protocol (tcp/udp/icmp) + * @return true if succeeded + * @throws PermissionDeniedException if user is not authorized to operate on the supplied IP address + * @throws InvalidParameterValueException if the supplied parameters have invalid values + * @throws InternalErrorException if the update could not be performed + */ + // boolean deleteIpForwardingRule(long userId, long accountId, String publicIp, String publicPort, String privateIp, String privatePort, String proto) throws PermissionDeniedException, InvalidParameterValueException, InternalErrorException; + + /** + * Delete a load balancing rule + * @param userId userId calling this api + * @param accountId accountId calling this api + * @param publicIp Ip address + * @param publicPort port + * @param privateIp private address (10.x.y.z) to be forwarded to + * @param privatePort private port to be forwarded to + * @param algo loadbalance algorithm (roundrobin/source/leastconn/etc) + * @return true if succeeded + * @throws PermissionDeniedException if user is not authorized to operate on the supplied IP address + * @throws InvalidParameterValueException if the supplied parameters have invalid values + * @throws InternalErrorException if the update could not be performed + */ + // boolean deleteLoadBalancingRule(long userId, long accountId, String publicIp, String publicPort, String privateIp, String privatePort, String algo) throws PermissionDeniedException, InvalidParameterValueException, InternalErrorException; + + /** + * Generates a random password that will be used (initially) by newly created and started virtual machines + * @return a random password + */ + String generateRandomPassword(); + + /** + * Resets the password for a virtual machine with a new password + * @param userId, the user that's reseting the password + * @param vmId the ID of the virtual machine + * @param password the password for the virtual machine + * @return true or false, based on the success of the method + */ + boolean resetVMPassword(long userId, long vmId, String password); + long resetVMPasswordAsync(long userId, long vmId, String password); + + /** + * Attaches the specified volume to the specified VM + * @param vmId + * @param volumeId + * @throws InvalidParameterValueException, InternalErrorException + */ + void attachVolumeToVM(long vmId, long volumeId, Long deviceId, long startEventId) throws InternalErrorException; + long attachVolumeToVMAsync(long vmId, long volumeId, Long deviceId) throws InvalidParameterValueException; + + /** + * Detaches the specified volume from the VM it is currently attached to. If it is not attached to any VM, will return true. + * @param vmId + * @volumeId + * @throws InvalidParameterValueException, InternalErrorException + */ + void detachVolumeFromVM(long volumeId, long startEventId) throws InternalErrorException; + long detachVolumeFromVMAsync(long volumeId) throws InvalidParameterValueException; + + /** + * Attaches an ISO to the virtual CDROM device of the specified VM. Will fail if the VM already has an ISO mounted. + * @param vmId + * @param userId + * @param isoId + * @param attach whether to attach or detach the iso from the instance + * @return + */ + boolean attachISOToVM(long vmId, long userId, long isoId, boolean attach, long startEventId); + long attachISOToVMAsync(long vmId, long userId, long isoId) throws InvalidParameterValueException; + long detachISOFromVMAsync(long vmId, long userId) throws InvalidParameterValueException; + + /** + * Creates and starts a new Virtual Machine. + * + * @param userId + * @param accountId + * @param dataCenterId + * @param serviceOfferingId + * @param templateId - the id of the template (or ISO) to use for creating the virtual machine + * @param diskOfferingId - ID of the disk offering to use when creating the root disk (if deploying from an ISO) or the data disk (if deploying from a template). If deploying from a template and a disk offering ID is not passed in, the VM will have only a root disk. + * @param domain the end user wants to use for this virtual machine. can be null. If the virtual machine is already part of an existing network, the domain is ignored. + * @param password the password that the user wants to use to access this virtual machine + * @param displayName user-supplied name to be shown in the UI or returned in the API + * @param groupName user-supplied groupname to be shown in the UI or returned in the API + * @param userData user-supplied base64-encoded data that can be retrieved by the instance from the virtual router + * @param size -- size to be used for volume creation in case the disk offering is private (i.e. size=0) + * @return VirtualMachine if successfully deployed, null otherwise + * @throws InvalidParameterValueException if the parameter values are incorrect. + * @throws ExecutionException + * @throws StorageUnavailableException + * @throws ConcurrentOperationException + */ + UserVm deployVirtualMachine(long userId, long accountId, long dataCenterId, long serviceOfferingId, long templateId, Long diskOfferingId, String domain, String password, String displayName, String group, String userData, String [] groups, long startEventId, long size) throws ResourceAllocationException, InvalidParameterValueException, InternalErrorException, InsufficientStorageCapacityException, PermissionDeniedException, ExecutionException, StorageUnavailableException, ConcurrentOperationException; + long deployVirtualMachineAsync(long userId, long accountId, long dataCenterId, long serviceOfferingId, long templateId, Long diskOfferingId, String domain, String password, String displayName, String group, String userData, String [] groups, long size) throws InvalidParameterValueException, PermissionDeniedException; + + /** + * Starts a Virtual Machine + * + * @param userId the id of the user performing the action + * @param vmId + * @param isoPath - path of the ISO file to boot this VM from (null to boot from root disk) + * @return VirtualMachine if successfully started, null otherwise + * @throws ExecutionException + * @throws StorageUnavailableException + * @throws ConcurrentOperationException + */ + UserVm startVirtualMachine(long userId, long vmId, String isoPath) throws InternalErrorException, ExecutionException, StorageUnavailableException, ConcurrentOperationException; + long startVirtualMachineAsync(long userId, long vmId, String isoPath); + + /** + * Stops a Virtual Machine + * + * @param userId the id of the user performing the action + * @param vmId + * @return true if successfully stopped, false otherwise + */ + boolean stopVirtualMachine(long userId, long vmId); + long stopVirtualMachineAsync(long userId, long vmId); + + /** + * Reboots a Virtual Machine + * + * @param vmId + * @return true if successfully rebooted, false otherwise + */ + boolean rebootVirtualMachine(long userId, long vmId); + + /** + * Reboots a Virtual Machine + * + * @param vmId + * @return the async-call job id + */ + long rebootVirtualMachineAsync(long userId, long vmId); + + + /** + * Destroys a Virtual Machine + * @param vmId + * @return true if destroyed, false otherwise + */ + boolean destroyVirtualMachine(long userId, long vmId); + + /** + * + * @param userId + * @param vmId + * @return the async-call job id + */ + long destroyVirtualMachineAsync(long userId, long vmId); + + /** + * Recovers a destroyed virtual machine. + * @param vmId + * @return true if recovered, false otherwise + * @throws InternalErrorException + */ + boolean recoverVirtualMachine(long vmId) throws ResourceAllocationException, InternalErrorException; + + /** + * Upgrade the virtual machine to a new service offering + * @param vmId + * @param serviceOfferingId + * @return success/failure + */ + boolean upgradeVirtualMachine(long userId, long vmId, long serviceOfferingId, long startEventId); + long upgradeVirtualMachineAsync(long userId, long vmId, long serviceOfferingId) throws InvalidParameterValueException; + + + /** + * Updates display name and group for virtual machine; enables/disabled ha + * @param vmId + * @param group, displayName + * @param enable true to enable HA, false otherwise + * @param userId - id of user performing the update on the virtual machine + * @param accountId - id of the account that owns the virtual machine + */ + void updateVirtualMachine(long vmId, String displayName, String group, boolean enable, Long userId, long accountId); + + /** + * Updates a storage pool. + * @param poolId ID of the storage pool to be updated + * @param tags Tags that will be added to the storage pool + */ + StoragePoolVO updateStoragePool(long poolId, String tags); + + /** + * Starts a Domain Router + * + * @param routerId + * @return DomainRouter if successfully started, false otherwise + */ + DomainRouter startRouter(long routerId, long startEventId) throws InternalErrorException; + long startRouterAsync(long routerId); + + /** + * Stops a Domain Router + * + * @param routerId + * @return true if successfully stopped, false otherwise + */ + boolean stopRouter(long routerId, long startEventId); + long stopRouterAsync(long routerId); + + /** + * Reboots a Domain Router + * + * @param routerId + * @return true if successfully rebooted, false otherwise + */ + boolean rebootRouter(long routerId, long startEventId) throws InternalErrorException; + long rebootRouterAsync(long routerId); + + /** + * Destroys a Domain Router + * + * @param routerId + * @return true if successfully destroyed, false otherwise + */ + boolean destroyRouter(long routerId); + + /** + * Finds a domain router by user and data center + * @param userId + * @param dataCenterId + * @return a list of DomainRouters + */ + DomainRouterVO findDomainRouterBy(long accountId, long dataCenterId); + + /** + * Finds a domain router by id + * @param router id + * @return a domainRouter + */ + DomainRouterVO findDomainRouterById(long domainRouterId); + + + /** + * Retrieves a data center by id + * + * @param dataCenterId + * @return DataCenter + */ + DataCenterVO getDataCenterBy(long dataCenterId); + + /** + * Retrieves a pod by id + * + * @param podId + * @return Pod + */ + HostPodVO getPodBy(long podId); + + /** + * Retrieves the list of all data centers + * @return a list of DataCenters + */ + List listDataCenters(); + + /** + * Retrieves a list of data centers that contain domain routers + * that the specified user owns. + * + * @param userId + * @return a list of DataCenters + */ + List listDataCentersBy(long userId); + + /** + * Retrieves a host by id + * @param hostId + * @return Host + */ + HostVO getHostBy(long hostId); + + /** + * Updates a host + * @param hostId + * @param guestOSCategoryId + */ + void updateHost(long hostId, long guestOSCategoryId) throws InvalidParameterValueException; + + /** + * Deletes a host + * + * @param hostId + * @param true if deleted, false otherwise + */ + boolean deleteHost(long hostId); + + /** + * Retrieves all Events between the start and end date specified + * + * @param userId unique id of the user, pass in -1 to retrieve events for all users + * @param accountId unique id of the account (which could be shared by many users), pass in -1 to retrieve events for all accounts + * @param domainId the id of the domain in which to search for users (useful when -1 is passed in for userId) + * @param type the type of the event. + * @param level INFO, WARN, or ERROR + * @param startDate inclusive. + * @param endDate inclusive. If date specified is greater than the current time, the + * system will use the current time. + * @return List of events + */ + List getEvents(long userId, long accountId, Long domainId, String type, String level, Date startDate, Date endDate); + + /** + * returns the a map of the names/values in the configuraton table + * @return map of configuration name/values + */ + List searchForConfigurations(Criteria c, boolean showHidden); + + /** + * returns the instance id of this management server. + * @return id of the management server + */ + long getId(); + + /** revisit + * Searches for users by the specified search criteria + * Can search by: "id", "username", "account", "domainId", "type" + * @param c + * @return List of UserAccounts + */ + List searchForUsers(Criteria c); + + /** + * Searches for Service Offerings by the specified search criteria + * Can search by: "name" + * @param c + * @return List of ServiceOfferings + */ + List searchForServiceOfferings(Criteria c); + + /** + * Searches for Clusters by the specified search criteria + * @param c + * @return + */ + List searchForClusters(Criteria c); + + /** + * Searches for Pods by the specified search criteria + * Can search by: pod name and/or zone name + * @param c + * @return List of Pods + */ + List searchForPods(Criteria c); + + /** + * Searches for Zones by the specified search criteria + * Can search by: zone name + * @param c + * @return List of Zones + */ + List searchForZones(Criteria c); + + /** + * Searches for servers by the specified search criteria + * Can search by: "name", "type", "state", "dataCenterId", "podId" + * @param c + * @return List of Hosts + */ + List searchForServers(Criteria c); + + /** + * Searches for servers that are either Down or in Alert state + * @param c + * @return List of Hosts + */ + List searchForAlertServers(Criteria c); + + /** + * Search for templates by the specified search criteria + * Can search by: "name", "ready", "isPublic" + * @param c + * @return List of VMTemplates + */ + List searchForTemplates(Criteria c); + + /** + * Lists the template host records by template Id + * + * @param templateId + * @param zoneId + * @return List of VMTemplateHostVO + */ + List listTemplateHostBy(long templateId, Long zoneId); + + /** + * Obtains pods that match the data center ID + * @param dataCenterId + * @return List of Pods + */ + List listPods(long dataCenterId); + + /** + * Creates a new service offering + * @param userId + * @param name + * @param cpu + * @param ramSize + * @param speed + * @param diskSpace + * @param displayText + * @param localStorageRequired + * @param offerHA + * @param useVirtualNetwork + * @return the new ServiceOfferingVO + */ + ServiceOfferingVO createServiceOffering(long userId, String name, int cpu, int ramSize, int speed, String displayText, boolean localStorageRequired, boolean offerHA, boolean useVirtualNetwork, String tags); + + /** + * Updates a service offering + * @param userId + * @param serviceOfferingId + * @param name + * @param displayText + * @param offerHA + * @param useVirtualNetwork + * @param tags tags for the service offering. if null, no change will be made. if empty string, all tags will be removed. + * @return the updated ServiceOfferingVO + */ + ServiceOfferingVO updateServiceOffering(long userId, long serviceOfferingId, String name, String displayText, Boolean offerHA, Boolean useVirtualNetwork, String tags); + + /** + * Deletes a service offering + * @param userId + * @param serviceOfferingId + * @return success/failure + */ + boolean deleteServiceOffering(long userId, long serviceOfferingId); + + /** + * Adds a new pod to the database + * @param userId + * @param podName + * @param zoneId + * @param gateway + * @param cidr + * @param startIp + * @param endIp + * @return Pod + */ + HostPodVO createPod(long userId, String podName, Long zoneId, String gateway, String cidr, String startIp, String endIp) throws InvalidParameterValueException, InternalErrorException; + + /** + * Edits a pod in the database + * @param userId + * @param podId + * @param newPodName + * @param gateway + * @param cidr + * @param startIp + * @param endIp + * @return Pod + */ + HostPodVO editPod(long userId, long podId, String newPodName, String gateway, String cidr, String startIp, String endIp) throws InvalidParameterValueException, InternalErrorException; + + /** + * Deletes a pod from the database + * @param userId + * @param podId + */ + void deletePod(long userId, long podId) throws InvalidParameterValueException, InternalErrorException; + + /** + * Adds a new zone to the database + * @param userId + * @param zoneName + * @param dns1 + * @param dns2 + * @param dns3 + * @param dns4 + * @param "-" separated range for network virtualization. + * @param guestNetworkCidr + * @return Zone + */ + DataCenterVO createZone(long userId, String zoneName, String dns1, String dns2, String dns3, String dns4, String vnetRange, String guestCidr) throws InvalidParameterValueException, InternalErrorException; + + /** + * Edits a zone in the database + * @param userId + * @param zoneId + * @param newZoneName + * @param dns1 + * @param dns2 + * @param dns3 + * @param dns4 + * @param vnetRange range of the vnet to add to the zone. + * @param guestNetworkCidr + * @return Zone + */ + DataCenterVO editZone(long userId, Long zoneId, String newZoneName, String dns1, String dns2, String dns3, String dns4, String vnetRange, String guestCidr) throws InvalidParameterValueException, InternalErrorException; + + /** + * Deletes a zone from the database + * @param userId + * @param zoneId + */ + void deleteZone(long userId, Long zoneId) throws InvalidParameterValueException, InternalErrorException; + + /** + * Change a pod's private IP range + * @param op + * @param podId + * @param startIP + * @param endIP + * @return Message to display to user + * @throws InvalidParameterValueException if unable to add private ip range + */ + String changePrivateIPRange(boolean add, Long podId, String startIP, String endIP) throws InvalidParameterValueException; + + // List searchUsers(String name); + + /** + * Finds users with usernames similar to the parameter + * @param username + * @return list of Users + */ + // List findUsersLike(String username); + + /** + * Finds a user by their user ID. + * @param ownerId + * @return User + */ + User findUserById(Long userId); + + /** + * Gets user by id. + * + * @param userId + * @param active + * @return + */ + User getUser(long userId, boolean active); + + /** + * Obtains a list of user statistics for the specified user ID. + * @param userId + * @return List of UserStatistics + */ + List listUserStatsBy(Long userId); + + /** + * Obtains a list of virtual machines that are similar to the VM with the specified name. + * @param vmInstanceName + * @return List of VMInstances + */ + List findVMInstancesLike(String vmInstanceName); + + /** + * Finds a virtual machine instance with the specified Volume ID. + * @param volumeId + * @return VMInstance + */ + VMInstanceVO findVMInstanceById(long vmId); + + /** + * Finds a guest virtual machine instance with the specified ID. + * @param userVmId + * @return UserVmVO + */ + UserVmVO findUserVMInstanceById(long userVmId); + + /** + * Finds a service offering with the specified ID. + * @param offeringId + * @return ServiceOffering + */ + ServiceOfferingVO findServiceOfferingById(long offeringId); + + /** + * Obtains a list of all service offerings. + * @return List of ServiceOfferings + */ + List listAllServiceOfferings(); + + /** + * Obtains a list of all active hosts. + * @return List of Hosts. + */ + List listAllActiveHosts(); + + /** + * Finds a data center with the specified ID. + * @param dataCenterId + * @return DataCenter + */ + DataCenterVO findDataCenterById(long dataCenterId); + + /** + * Finds a VLAN with the specified ID. + * @param vlanDbId + * @return VLAN + */ + VlanVO findVlanById(long vlanDbId); + + /** + * Creates a new template with the specified parameters + * @param id + * @param name + * @param displayText + * @param String format + * @param Long guestOsId + * @param Boolean passwordEnabled + * @param Boolean bootable + * @return success/failure + */ + boolean updateTemplate(Long id, String name, String displayText, String format, Long guestOsId, Boolean passwordEnabled, Boolean bootable) throws InvalidParameterValueException; + + /** + * Creates a template by downloading to all zones + * @param createdBy userId of the template creater + * @param zoneId optional zoneId. if null, assumed to be all zones + * @param name - user specified name for the template + * @param displayText user readable name. + * @param isis it public + * @param featured is it featured + * @param format format of the template (VHD, ISO, QCOW2, etc) + * @param diskType filesystem such as ext2, ntfs etc + * @param url url to download from + * @param chksum checksum to be verified + * @param requiresHvm + * @param bits - 32 or 64 bit template + * @param enablePassword should password generation be enabled + * @param guestOSId guestOS id + * @param bootable is the disk bootable + * @return template id of created template + * @throws IllegalArgumentException + * @throws ResourceAllocationException + * @throws InvalidParameterValueException + */ + Long createTemplate(long createdBy, Long zoneId, String name, String displayText, boolean isPublic, boolean featured, String format, String diskType, String url, String chksum, boolean requiresHvm, int bits, boolean enablePassword, long guestOSId, boolean bootable) throws IllegalArgumentException, ResourceAllocationException, InvalidParameterValueException; + + /** + * Deletes a template from all secondary storage servers + * @param userId + * @param templateId + * @param zoneId + * @return true if success + */ + boolean deleteTemplate(long userId, long templateId, Long zoneId, long startEventId) throws InternalErrorException; + long deleteTemplateAsync(long userId, long templateId, Long zoneId) throws InvalidParameterValueException; + + /** + * Copies a template from one secondary storage server to another + * @param userId + * @param templateId + * @param sourceZoneId - the source zone + * @param destZoneId - the destination zone + * @return true if success + * @throws InternalErrorException + */ + boolean copyTemplate(long userId, long templateId, long sourceZoneId, long destZoneId, long startEventId) throws InternalErrorException; + long copyTemplateAsync(long userId, long templateId, long sourceZoneId, long destZoneId) throws InvalidParameterValueException; + + /** + * Deletes an ISO from all secondary storage servers + * @param userId + * @param isoId + * @param zoneId + * @return true if success + */ + long deleteIsoAsync(long userId, long isoId, Long zoneId) throws InvalidParameterValueException; + + /** + * Finds a template by the specified ID. + * @param templateId + * @return A VMTemplate + */ + VMTemplateVO findTemplateById(long templateId); + + /** + * Finds a template-host reference by the specified template and zone IDs + * @param templateId + * @param zoneId + * @return template-host reference + */ + VMTemplateHostVO findTemplateHostRef(long templateId, long zoneId); + + /** + * Obtains a list of virtual machines that match the specified host ID. + * @param hostId + * @return List of UserVMs. + */ + List listUserVMsByHostId(long hostId); + + /** + * Obtains a list of virtual machines by the specified search criteria. + * Can search by: "userId", "name", "state", "dataCenterId", "podId", "hostId" + * @param c + * @return List of UserVMs. + */ + List searchForUserVMs(Criteria c); + + /** + * Obtains a list of firewall rules by the specified IP address and forwarding flag. + * @param publicIPAddress + * @param forwarding + * @return + */ + List listIPForwarding(String publicIPAddress, boolean forwarding); + + /** + * Create a single port forwarding rule from the given ip address and port to the vm's guest IP address and private port with the given protocol. + * @param userId the id of the user performing the action (could be an admin's ID if performing on behalf of a user) + * @param ipAddressVO + * @param userVM + * @param publicPort + * @param privatePort + * @param protocol + * @return + */ + FirewallRuleVO createPortForwardingRule(long userId, IPAddressVO ipAddressVO, UserVmVO userVM, String publicPort, String privatePort, String protocol) throws NetworkRuleConflictException; + + /** + * Update an existing port forwarding rule on the given public IP / public port for the given protocol + * @param userId id of the user performing the action + * @param publicIp ip address of the forwarding rule to update + * @param privateIp ip address to forward to + * @param publicPort public port of the forwarding rule to update + * @param privatePort private port to forward to + * @param protocol protocol of the rule to update + * @return the new firewall rule if updated, null if no rule on public IP / public port of that protocol could be found + */ + FirewallRuleVO updatePortForwardingRule(long userId, String publicIp, String privateIp, String publicPort, String privatePort, String protocol); + long updatePortForwardingRuleAsync(long userId, long accountId, String publicIp, String privateIp, String publicPort, String privatePort, String protocol); + + /** + * Find a firewall rule by rule id + * @param ruleId + * @return + */ + FirewallRuleVO findForwardingRuleById(Long ruleId); + + /** + * Find an IP Address VO object by ip address string + * @param ipAddress + * @return IP Address VO object corresponding to the given address string, null if not found + */ + IPAddressVO findIPAddressById(String ipAddress); + + /** + * Search for network rules given the search criteria. For now only group id (security group id) is supported. + * @param c the search criteria including order by and max rows + * @return list of rules for the security group id specified in the search criteria + */ + List searchForNetworkRules(Criteria c); + + /** + * Saves an event with the specified parameters. + * @param userId + * @param accountId + * @param type + * @param description + * @return ID of the saved event. + */ + // Long saveEvent(Long userId, long accountId, String level, String type, String description, String params); + + /** + * Obtains a list of events by the specified search criteria. + * Can search by: "username", "type", "level", "startDate", "endDate" + * @param c + * @return List of Events. + */ + List searchForEvents(Criteria c); + + List listPendingEvents(int entryTime, int duration); + + /** + * Obtains a list of routers by the specified host ID. + * @param hostId + * @return List of DomainRouters. + */ + List listRoutersByHostId(long hostId); + + /** + * Obtains a list of all active routers. + * @return List of DomainRouters + */ + List listAllActiveRouters(); + + /** + * Obtains a list of routers by the specified search criteria. + * Can search by: "userId", "name", "state", "dataCenterId", "podId", "hostId" + * @param c + * @return List of DomainRouters. + */ + List searchForRouters(Criteria c); + + List searchForConsoleProxy(Criteria c); + + /** + * Finds a volume which is not destroyed or removed. + */ + VolumeVO findVolumeById(long id); + + /** + * Return the volume with the given id even if its destroyed or removed. + */ + VolumeVO findAnyVolumeById(long volumeId); + + /** revisit + * Obtains a list of storage volumes by the specified search criteria. + * Can search by: "userId", "vType", "instanceId", "dataCenterId", "podId", "hostId" + * @param c + * @return List of Volumes. + */ + List searchForVolumes(Criteria c); + + /** + * Checks that the volume is stored on a shared storage pool. + * @param volumeId + * @return true if the volume is on a shared storage pool, false otherwise + */ + boolean volumeIsOnSharedStorage(long volumeId) throws InvalidParameterValueException; + + /** + * Finds a pod by the specified ID. + * @param podId + * @return HostPod + */ + HostPodVO findHostPodById(long podId); + + /** + * Finds a secondary storage host in the specified zone + * @param zoneId + * @return Host + */ + HostVO findSecondaryStorageHosT(long zoneId); + + /** + * Obtains a list of IP Addresses by the specified search criteria. + * Can search by: "userId", "dataCenterId", "address" + * @param sc + * @return List of IPAddresses + */ + List searchForIPAddresses(Criteria c); + + /** + * Obtains a list of billing records by the specified search criteria. + * Can search by: "userId", "startDate", "endDate" + * @param c + * @return List of Billings. + List searchForUsage(Criteria c); + */ + + /** + * Obtains a list of all active DiskTemplates. + * @return list of DiskTemplates + */ + List listAllActiveDiskTemplates(); + + /** + * Obtains a list of all templates. + * @return list of VMTemplates + */ + List listAllTemplates(); + + /** + * Obtains a list of all guest OS. + * @return list of GuestOS + */ + List listGuestOSByCriteria(Criteria c); + + /** + * Obtains a list of all guest OS categories. + * @return list of GuestOSCategories + */ + List listGuestOSCategoriesByCriteria(Criteria c); + + /** + * Logs out a user + * @param userId + */ + void logoutUser(Long userId); + + /** + * Updates a configuration value. + * @param userId + * @param name + * @param value + * @return + */ + void updateConfiguration(long userId, String name, String value) throws InvalidParameterValueException, InternalErrorException; + + /** + * Creates or updates an IP forwarding or load balancer rule. + * @param isForwarding if true, an IP forwarding rule will be created/updated, else a load balancer rule will be created/updated + * @param address + * @param port + * @param privateIPAddress + * @param privatePort + * @param protocol + * @return the rule if it was successfully created + * @throws InvalidParameterValueException + * @throws PermissionDeniedException + * @throws NetworkRuleConflictException + * @throws InternalErrorException + */ + NetworkRuleConfigVO createOrUpdateRule(long userId, long securityGroupId, String address, String port, String privateIpAddress, String privatePort, String protocol, String algorithm) + throws InvalidParameterValueException, PermissionDeniedException, NetworkRuleConflictException, InternalErrorException; + long createOrUpdateRuleAsync(boolean isForwarding, long userId, long accountId, Long domainId, long securityGroupId, String address, + String port, String privateIpAddress, String privatePort, String protocol, String algorithm); + + /** + * Deletes an IP forwarding or load balancer rule + * @param ruleId + * @param userId + * @param accountId + * @throws InvalidParameterValueException + * @throws PermissionDeniedException + * @throws InternalErrorException + */ + void deleteRule(long id, long userId, long accountId) throws InvalidParameterValueException, PermissionDeniedException, InternalErrorException; + long deleteRuleAsync(long id, long userId, long accountId); + + ConsoleProxyInfo getConsoleProxy(long dataCenterId, long userVmId); + ConsoleProxyVO startConsoleProxy(long instanceId, long startEventId) throws InternalErrorException; + long startConsoleProxyAsync(long instanceId); + boolean stopConsoleProxy(long instanceId, long startEventId); + long stopConsoleProxyAsync(long instanceId); + boolean rebootConsoleProxy(long instanceId, long startEventId); + long rebootConsoleProxyAsync(long instanceId); + boolean destroyConsoleProxy(long instanceId, long startEventId); + long destroyConsoleProxyAsync(long instanceId); + String getConsoleAccessUrlRoot(long vmId); + ConsoleProxyVO findConsoleProxyById(long instanceId); + VMInstanceVO findSystemVMById(long instanceId); + boolean stopSystemVM(long instanceId, long startEventId); + VMInstanceVO startSystemVM(long instanceId, long startEventId) throws InternalErrorException; + long startSystemVmAsync(long instanceId); + long stopSystemVmAsync(long instanceId); + long rebootSystemVmAsync(long longValue); + boolean rebootSystemVM(long instanceId, long startEventId); + + + + + /** + * Returns a configuration value with the specified name + * @param name + * @return configuration value + */ + String getConfigurationValue(String name); + + /** + * Returns the vnc port of the vm. + * + * @param VirtualMachine vm + * @return the vnc port if found; -1 if unable to find. + */ + int getVncPort(VirtualMachine vm); + + + /** + * Search for domains owned by the given domainId/domainName (those parameters are wrapped + * in a Criteria object. + * @return list of domains owned by the given user + */ + List searchForDomains(Criteria c); + + List searchForDomainChildren(Criteria c); + + /** + * create a new domain + * @param id + * @param domain name + * @param ownerId + * @param parentId + * + */ + DomainVO createDomain(String name, Long ownerId, Long parentId); + + /** + * delete a domain with the given domainId + * @param domainId + * @param ownerId + * @param cleanup - whether or not to delete all accounts/VMs/sub-domains when deleting the domain + */ + String deleteDomain(Long domainId, Long ownerId, Boolean cleanup); + long deleteDomainAsync(Long domainId, Long ownerId, Boolean cleanup); + /** + * update an existing domain + * @param domainId the id of the domain to be updated + * @param domainName the new name of the domain + */ + void updateDomain(Long domainId, String domainName); + + /** + * find the domain Id associated with the given account + * @param accountId the id of the account to use to look up the domain + */ + Long findDomainIdByAccountId(Long accountId); + + /** + * find the domain by id + * @param domainId the id of the domainId + */ + DomainVO findDomainIdById(Long domainId); + + /** + * find the domain by its path + * @param domainPath the path to use to lookup a domain + * @return domainVO the domain with the matching path, or null if no domain with the given path exists + */ + DomainVO findDomainByPath(String domainPath); + + /** + * Finds accounts with account identifiers similar to the parameter + * @param accountName + * @return list of Accounts + */ + List findAccountsLike(String accountName); + + /** + * Finds accounts with account identifier + * @param accountName + * @return an account that is active (not deleted) + */ + Account findActiveAccountByName(String accountName); + + /** + * Finds accounts with account identifier + * @param accountName, domainId + * @return an account that is active (not deleted) + */ + + Account findActiveAccount(String accountName, Long domainId); + + /** + * Finds accounts with account identifier + * @param accountName + * @param domainId + * @return an account that may or may not have been deleted + */ + Account findAccountByName(String accountName, Long domainId); + + /** + * Finds an account by the ID. + * @param accountId + * @return Account + */ + Account findAccountById(Long accountId); + + /** + * Finds a GuestOS by the ID. + * @param id + * @return GuestOS + */ + GuestOS findGuestOSById(Long id); + + /** + * Searches for accounts by the specified search criteria + * Can search by: "id", "name", "domainid", "type" + * @param c + * @return List of Accounts + */ + List searchForAccounts(Criteria c); + + + /** + * Find the owning account of an IP Address + * @param ipAddress + * @return owning account if ip address is allocated, null otherwise + */ + Account findAccountByIpAddress(String ipAddress); + + /** + * Updates an existing resource limit with the specified details. If a limit doesn't exist, will create one. + * @param domainId + * @param accountId + * @param type + * @param max + * @return + * @throws InvalidParameterValueException + */ + ResourceLimitVO updateResourceLimit(Long domainId, Long accountId, ResourceType type, Long max) throws InvalidParameterValueException; + + /** + * Deletes a Limit + * @param limitId - the database ID of the Limit + * @return true if successful, false if not + */ + boolean deleteLimit(Long limitId); + + /** + * Finds limit by id + * @param limitId - the database ID of the Limit + * @return LimitVO object + */ + ResourceLimitVO findLimitById(long limitId); + + /** + * Searches for Limits. + * @param domainId + * @param accountId + * @param type + * @return a list of Limits + */ + List searchForLimits(Criteria c); + + /** + * Finds the correct limit for an account. I.e. if an account's limit is not present, it will check the account's domain, and as a last resort use the global limit. + * @param type + * @param accountId + */ + long findCorrectResourceLimit(ResourceType type, long accountId); + + /** + * Gets the count of resources for a resource type and account + * @param Type + * @param accountId + * @return count of resources + */ + long getResourceCount(ResourceType type, long accountId); + + /** + * Lists ISOs that are available for the specified account ID. + * @param accountId + * @param accountType + * @return a list of ISOs (VMTemplateVO objects) + */ + List listIsos(Criteria c); + + /** + * Searches for alerts + * @param c + * @return List of Alerts + */ + List searchForAlerts(Criteria c); + + /** + * list all the capacity rows in capacity operations table + * @param c + * @return List of capacities + */ + List listCapacities(Criteria c); + + public long getMemoryUsagebyHost(Long hostId); + + /** + * Create a snapshot of a volume + * @param userId the user for whom this snapshot is being created + * @param volumeId the id of the volume + * @return the Snapshot that was created + * @throws InternalErrorException + */ + long createSnapshotAsync(long userId, long volumeId) + throws InvalidParameterValueException, + ResourceAllocationException, + InternalErrorException; + + /** + * @param userId The Id of the user who invoked this operation. + * @param volumeId The volume for which this snapshot is being taken + * @return The properties of the snapshot taken + */ + SnapshotVO createTemplateSnapshot(Long userId, long volumeId); + + /** + * Destroy a snapshot + * @param snapshotId the id of the snapshot to destroy + * @return true if snapshot successfully destroyed, false otherwise + */ + boolean destroyTemplateSnapshot(Long userId, long snapshotId); + long deleteSnapshotAsync(long userId, long snapshotId); + + long createVolumeFromSnapshotAsync(long userId, long accountId, long snapshotId, String volumeName) throws InternalErrorException, ResourceAllocationException; + + /** + * List all snapshots of a disk volume. Optionaly lists snapshots created by specified interval + * @param c the search criteria (order by, limit, etc.) + * @return list of snapshots + * @throws InvalidParameterValueException + */ + List listSnapshots(Criteria c, String interval) throws InvalidParameterValueException; + + /** + * find a single snapshot by id + * @param snapshotId + * @return the snapshot if found, null otherwise + */ + Snapshot findSnapshotById(long snapshotId); + + /** + * Create a private template from a given snapshot + * @param snapshotId the id of the snapshot to use as the basis of the template + * @param name user provided string to use to name the template + * @param description the display text to show when listing the template as given by the user + * @param guestOSId the OS of the template + * @param requiresHvm whether the new template will require HVM + * @param bits number of bits (32-bit or 64-bit) + * @param passwordEnabled whether or not the template is password enabled + * @param iswhether or not the template is public + * @return valid template if success, null otherwise + * @throws InvalidParameterValueException, ResourceAllocationException + */ + VMTemplateVO createPrivateTemplate(VMTemplateVO template, Long userId, long snapshotId, String name, String description) throws InvalidParameterValueException; + long createPrivateTemplateAsync(Long userId, long vmId, String name, String description, long guestOSId, Boolean requiresHvm, Integer bits, Boolean passwordEnabled, boolean isPublic, boolean featured, Long snapshotId) throws InvalidParameterValueException, ResourceAllocationException, InternalErrorException; + + + /** + * Finds a diskOffering by the specified ID. + * @param diskOfferingId + * @return A DiskOffering + */ + DiskOfferingVO findDiskOfferingById(long diskOffering); + + /** + * Finds the obj associated with the private disk offering + * @return -- vo obj for private disk offering + */ + List findPrivateDiskOffering(); + + /** + * Update the permissions on a template. A private template can be made public, or individual accounts can be granted permission to launch instances from the template. + * @param templateId + * @param operation + * @param isPublic + * @param isFeatured + * @param accountNames + * @return + * @throws InvalidParameterValueException + * @throws PermissionDeniedException + * @throws InternalErrorException + */ + boolean updateTemplatePermissions(long templateId, String operation, Boolean isPublic, Boolean isFeatured, List accountNames) throws InvalidParameterValueException, PermissionDeniedException, InternalErrorException; + + /** + * List the permissions on a template. This will return a list of account names that have been granted permission to launch instances from the template. + * @param templateId + * @return list of account names that have been granted permission to launch instances from the template + */ + List listTemplatePermissions(long templateId); + + /** + * List private templates for which the given account/domain has been granted permission to launch instances + * @param accountId + * @return + */ + List listPermittedTemplates(long accountId); + + /** + * Lists templates that match the specified criteria + * @param templateId - (optional) id of the template to return template host references for + * @param name a name (possibly partial) to search for + * @param keyword a keyword (using partial match) to search for, currently only searches name + * @param templateFilter - the category of template to return + * @param isIso whether this is an ISO search or non-ISO search + * @param bootable if null will return both bootable and non-bootable ISOs, else will return only one or the other, depending on the boolean value + * @param accountId parameter to use when searching for owner of template + * @param pageSize size of search results + * @param startIndex index in to search results to use + * @param zoneId optional zoneid to limit search + * @return list of templates + */ + List listTemplates(Long templateId, String name, String keyword, TemplateFilter templateFilter, boolean isIso, Boolean bootable, Long accountId, Integer pageSize, Long startIndex, Long zoneId) throws InvalidParameterValueException; + + /** + * Search for disk offerings based on search criteria + * @param c the criteria to use for searching for disk offerings + * @return a list of disk offerings that match the given criteria + */ + List searchForDiskOfferings(Criteria c); + + /** + * Create a disk offering + * @param domainId the id of the domain in which the disk offering is valid + * @param name the name of the disk offering + * @param description a string description of the disk offering + * @param numGibibytes the number of gibibytes in the disk offering (1 gibibyte = 1024 MB) + * @param mirrored boolean value of whether or not the offering provides disk mirroring + * @param tags Comma separated string to indicate special tags for the disk offering. + * @return the created disk offering, null if failed to create + */ + DiskOfferingVO createDiskOffering(long domainId, String name, String description, int numGibibytes, String tags) throws InvalidParameterValueException; + + /** + * Delete a disk offering + * @param id id of the disk offering to delete + * @return true if deleted, false otherwise + */ + boolean deleteDiskOffering(long id); + + /** + * Update a disk offering + * @param userId + * @param disk offering id + * @param name the name of the disk offering to be updated + * @param description a string description of the disk offering to be updated + * @param tags for the disk offering. if null, no change will be made. if empty string, all tags will be removed. + * @return updated disk offering + */ + DiskOfferingVO updateDiskOffering(long userId, long diskOfferingId, String name, String description, String tags); + + /** + * + * @param jobId async-call job id + * @return async-call result object + */ + AsyncJobResult queryAsyncJobResult(long jobId) throws PermissionDeniedException; + AsyncJobVO findInstancePendingAsyncJob(String instanceType, long instanceId); + AsyncJobVO findAsyncJobById(long jobId); + + List searchForAsyncJobs(Criteria c); + + + /** + * Assign a security group to a VM + * @param userId id of the user assigning the security group + * @param securityGroupId the id of the security group to apply (single add) + * @param securityGroupIdList the list of ids of the security groups that should be assigned to the vm (will add missing groups and remove existing groups to reconcile with the given list) + * @param publicIp ip address used for creating forwarding rules from the network rules in the group + * @param vmId vm id to use from getting the private ip address used for creating forwarding rules from the network rules in the group + */ + void assignSecurityGroup(Long userId, Long securityGroupId, List securityGroupIdList, String publicIp, Long vmId, long startEventId) throws PermissionDeniedException, NetworkRuleConflictException, InvalidParameterValueException, InternalErrorException; + + /** + * remove a security group from a publicIp/vmId combination where it had been previously applied + * @param userId id of the user performing the action (for events) + * @param securityGroupId the id of the security group to remove + * @param publicIp + * @param vmId + */ + void removeSecurityGroup(long userId, long securityGroupId, String publicIp, long vmId, long startEventId) throws InvalidParameterValueException, PermissionDeniedException; + + long assignSecurityGroupAsync(Long userId, Long securityGroupId, List securityGroupIdList, String publicIp, Long vmId); + + long removeSecurityGroupAsync(Long userId, long securityGroupId, String publicIp, long vmId); + + /** + * validate that the list of security groups can be applied to the instance + * @param securityGroupIds + * @param instanceId + * @return accountId that owns the instance if the security groups can be applied to the instance, null otherwise + */ + Long validateSecurityGroupsAndInstance(List securityGroupIds, Long instanceId); + + /** + * returns a list of security groups that can be applied to virtual machines for the given + * account/domain + * @param accountId the id of the account used for looking up groups + * @param domainId the domain of the given account, or if the account is null the domain + * to use for searching for groups + * @return a list of security groups + */ + List listSecurityGroups(Long accountId, Long domainId); + + /** + * returns a list of security groups + * @param c + * @return a list of security groups + */ + List searchForSecurityGroups(Criteria c); + + /** + * returns a list of security groups from a given ip and vm id + * @param c + * @return a list of security groups + */ + Map> searchForSecurityGroupsByVM(Criteria c); + + /** + * Create a security group, a group of network rules (port, private port, protocol, algorithm) that can be applied in mass to a VM + * @param name name of the group, must be unique for the domain + * @param description brief description of the group, can be null + * @param domainId domain where the security group is valid + * @param accountId owner of the security group, can be null for domain level security groups + * @return + */ + SecurityGroupVO createSecurityGroup(String name, String description, Long domainId, Long accountId); + + /** + * Delete a security group. If the group is being actively used, it cannot be deleted. + * @param userId the id of the user performing the action + * @param securityGroupId the id of the group to delete + * @param eventId + * @return true if the security group is deleted, exception is thrown otherwise + */ + boolean deleteSecurityGroup(long userId, long securityGroupId, long eventId) throws InvalidParameterValueException, PermissionDeniedException; + long deleteSecurityGroupAsync(long userId, Long accountId, long securityGroupId); + + /** + * check if a security group name in the given account/domain is in use + * - if accountId is specified, look only for the account + * - otherwise look for the name in domain-level security groups (accountId is null) + * @param domainId id of the domain in which to search for security groups + * @param accountId id of the account in which to search for security groups + * @param name name of the security group to look for + * @return true if the security group name is found, false otherwise + */ + boolean isSecurityGroupNameInUse(Long domainId, Long accountId, String name); + SecurityGroupVO findSecurityGroupById(Long groupId); + + boolean deleteNetworkRuleConfig(long userId, long networkRuleId); + long deleteNetworkRuleConfigAsync(long userId, Account account, Long networkRuleId) throws PermissionDeniedException; + + LoadBalancerVO findLoadBalancer(Long accountId, String name); + LoadBalancerVO findLoadBalancerById(long loadBalancerId); + List listLoadBalancerInstances(long loadBalancerId, boolean applied); + List searchForLoadBalancers(Criteria c); + LoadBalancerVO createLoadBalancer(Long userId, Long accountId, String name, String description, String ipAddress, String publicPort, String privatePort, String algorithm) throws InvalidParameterValueException, PermissionDeniedException; + boolean deleteLoadBalancer(long userId, long loadBalancerId); + long deleteLoadBalancerAsync(long userId, long loadBalancerId); + + /** + * Update a load balancer rule from the existing private port to a new private port. The load balancer is found by publicIp, public port, and algorithm. + * The individual rule for update is matched by privateIp. + * @param userId the id of the user performing the action + * @param loadBalancer the load balancer rule being updated + * @param privatePort the target private port for the load balancer rule (the rule will be updated from the existing port to this port) + * @param algorithm the target algorithm of the load balancer rule (the rule will be updated from the existing algorithm to this algorithm) + * @return the updated load balancer rule + */ + LoadBalancerVO updateLoadBalancerRule(long userId, LoadBalancerVO loadBalancer, String privatePort, String algorithm); + + /** + * Update the name and/or description of a load balancer rule + * @param loadBalancer the load balancer rule to update + * @param name the new name, null if not changing the name + * @param description the new description, null if not changing the description + * @return the updated load balancer rule + */ + LoadBalancerVO updateLoadBalancerRule(LoadBalancerVO loadBalancer, String name, String description) throws InvalidParameterValueException; + + /** + * Update the name, description, private port, and/or algorithm of a load balancer rule + * @param userId the id of the user performing the action + * @param accountId the id of the account that owns the load balancer rule + * @param loadBalancerId the id of the load balancer rule being updated + * @param name the new name, null if not changing the name + * @param description the new description, null if not changing the description + * @param privatePort the target private port for the load balancer rule (the rule will be updated from the existing port to this port) + * @param algorithm the target algorithm of the load balancer rule (the rule will be updated from the existing algorithm to this algorithm) + * @return the updated load balancer rule + */ + long updateLoadBalancerRuleAsync(long userId, long accountId, long loadBalancerId, String name, String description, String privatePort, String algorithm); + + void assignToLoadBalancer(long userId, long loadBalancerId, List instanceIds) throws NetworkRuleConflictException, InternalErrorException, PermissionDeniedException, InvalidParameterValueException; + long assignToLoadBalancerAsync(long userId, long loadBalancerId, List instanceIds); + boolean removeFromLoadBalancer(long userId, long loadBalancerId, List instanceIds) throws InvalidParameterValueException; + long removeFromLoadBalancerAsync(long userId, long loadBalancerId, List instanceIds); + + String[] getApiConfig(); + StoragePoolVO findPoolById(Long id); + StoragePoolVO addPool(Long zoneId, Long podId, Long clusterId, String poolName, String storageUri, String tags, Map details) throws ResourceInUseException, URISyntaxException, IllegalArgumentException, UnknownHostException, ResourceAllocationException; + List searchForStoragePools(Criteria c); + + /** + * Creates a policy with specified schedule to create snapshot for a volume . maxSnaps specifies the number of most recent snapshots that are to be retained. + * @param volumeId + * @param schedule MM[:HH][:DD] format. DD is day of week for weekly[1-7] and day of month for monthly + * @param interval hourly/daily/weekly/monthly + * @param maxSnaps If the number of snapshots go beyond maxSnaps the oldest snapshot is deleted + * @param timezone The timezone in which the above time format is specified + * @return + * @throws InvalidParameterValueException + */ + SnapshotPolicyVO createSnapshotPolicy(long userId, long accountId, long volumeId, String schedule, String intervalType, + int maxSnaps, String timezone) throws InvalidParameterValueException; + + /** + * List all snapshot policies which are created for the specified volume + * @param volumeId + * @return + */ + List listSnapshotPolicies(long volumeId); + SnapshotPolicyVO findSnapshotPolicyById(Long policyId); + + /** + * Deletes snapshot scheduling policies + */ + boolean deleteSnapshotPolicies(long userId, List policyIds) throws InvalidParameterValueException; + + /** + * Get the recurring snapshots scheduled for this volume currently along with the time at which they are scheduled + * @param volumeId The volume for which the snapshots are required. + * @param policyId Show snapshots for only this policy. + * @return The list of snapshot schedules. + */ + List findRecurringSnapshotSchedule(Long volumeId, Long policyId); + + /** + * Return whether a domain is a child domain of a given domain. + * @param parentId + * @param childId + * @return True if the domainIds are equal, or if the second domain is a child of the first domain. False otherwise. + */ + boolean isChildDomain(Long parentId, Long childId); + + + /** + * List interval types the specified snapshot belongs to + * @param snapshotId + * @return + */ + String getSnapshotIntervalTypes(long snapshotId); + + + List searchForSecondaryStorageVm(Criteria c); + + /** + * Deletes a pool based on the pool id + * @param id -- pool id + * @return -- status of the operation + */ + boolean deletePool(Long id); + + /** + * Returns back a SHA1 signed response + * @param userId -- id for the user + * @return -- ArrayList of + */ + ArrayList getCloudIdentifierResponse(long userId); + + /** + * check if a network security group name in the given account/domain is in use + * - if accountId is specified, look only for the account + * - otherwise look for the name in domain-level security groups (accountId is null) + * @param domainId id of the domain in which to search for security groups + * @param accountId id of the account in which to search for security groups + * @param name name of the security group to look for + * @return true if the security group name is found, false otherwise + */ + boolean isNetworkSecurityGroupNameInUse(Long domainId, Long accountId, String name); + NetworkGroupVO findNetworkGroupByName(Long accountId, String groupName); + + /** + * Find a network group by id + * @param networkGroupId id of group to lookup + * @return the network group if found, null otherwise + */ + NetworkGroupVO findNetworkGroupById(long networkGroupId); + + /** + * Authorize access to a network group. Access can be granted to a set of IP ranges, or to network groups belonging to other accounts. + * @param accountId the account id of the owner of the given network group + * @param groupName the name of the network group from which access is being granted + * @param protocol scopes the network protocol to which access is being granted + * @param startPort scopes the start of a network port range to which access is being granted (or icmp type if the protocol is icmp) + * @param endPort scopes the end of a network port range to which access is being granted (or icmp code if the protocol is icmp) + * @param cidrList the IP range to which access is being granted + * @param authorizedGroups the network groups (looked up by group name/account) to which access is being granted + * @return the job id if scheduled, 0 if the job was not scheduled + */ + long authorizeNetworkGroupIngressAsync(Long accountId, String groupName, String protocol, int startPort, int endPort, String [] cidrList, List authorizedGroups); + List authorizeNetworkGroupIngress(AccountVO account, String groupName, String protocol, int startPort, int endPort, String [] cidrList, List authorizedGroups); + + /** + * Revoke access to a network group. Access could have been granted to a set of IP ranges, or to network groups belonging to other accounts. Access + * can be revoked in a similar manner (either from a set of IP ranges or from network groups belonging to other accounts). + * @param accountId the account id of the owner of the given network group + * @param groupName the name of the network group from which access is being revoked + * @param protocol access had been granted on a port range (start port, end port) and network protocol, this protocol scopes the network protocol from which access is being revoked + * @param startPort access had been granted on a port range (start port, end port) and network protocol, this start port scopes the start of a network port range from which access is being revoked + * @param endPort access had been granted on a port range (start port, end port) and network protocol, this end port scopes the end of a network port range from which access is being revoked + * @param cidrList the IP range from which access is being revoked + * @param authorizedGroups the network groups (looked up by group name/account) from which access is being revoked + * @return the job id if scheduled, 0 if the job was not scheduled + */ + long revokeNetworkGroupIngressAsync(Long accountId, String groupName, String protocol, int startPort, int endPort, String [] cidrList, List authorizedGroups); + boolean revokeNetworkGroupIngress(AccountVO account, String groupName, String protocol, int startPort, int endPort, String [] cidrList, List authorizedGroups); + + NetworkGroupVO createNetworkGroup(String name, String description, Long domainId, Long accountId, String accountName); + + /** + * Delete an empty network group. If the group is not empty an error is returned. + * @param groupId + * @param accountId + * @throws PermissionDeniedException + */ + void deleteNetworkGroup(Long groupId, Long accountId) throws ResourceInUseException, PermissionDeniedException; + + /** + * Search for network groups and associated ingress rules for the given account, domain, group name, and/or keyword. + * The search terms are specified in the search criteria. + * @return the list of network groups and associated ingress rules + */ + public List searchForNetworkGroupRules(Criteria c); + + HostStats getHostStatistics(long hostId); + + /** + * Is the hypervisor snapshot capable. + * @return True if the hypervisor.type is XenServer + */ + boolean isHypervisorSnapshotCapable(); + List searchForStoragePoolDetails(long poolId, String value); + + /** + * Returns a comma separated list of tags for the specified storage pool + * @param poolId + * @return comma separated list of tags + */ + String getStoragePoolTags(long poolId); + + /** + * Checks if a host has running VMs that are using its local storage pool. + * @return true if local storage is active on the host + */ + boolean isLocalStorageActiveOnHost(HostVO host); + + public List getPreAllocatedLuns(Criteria c); + + public String getNetworkGroupsNamesForVm(long vmId); + + /** + * Persists the Event as a completed event. + * @return EventId of the persisted event + */ + public Long saveEvent(Long userId, Long accountId, String level, String type, String description, String params, long startEventId); + + /** + * Persists the Event as a Started event. + * @return EventId of the persisted event + */ + public Long saveStartedEvent(Long userId, Long accountId, String type, String description, long startEventId); + + boolean checkLocalStorageConfigVal(); + + boolean addConfig(String instance, String component, String category, String name, String value, String description); + + boolean validateCustomVolumeSizeRange(long size) throws InvalidParameterValueException; +} diff --git a/core/src/com/cloud/service/ServiceOfferingVO.java b/core/src/com/cloud/service/ServiceOfferingVO.java new file mode 100644 index 00000000000..8466ce82914 --- /dev/null +++ b/core/src/com/cloud/service/ServiceOfferingVO.java @@ -0,0 +1,145 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.service; + +import javax.persistence.Column; +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.Table; +import javax.persistence.Transient; + +import com.cloud.offering.ServiceOffering; +import com.cloud.storage.DiskOfferingVO; + +@Entity +@Table(name="service_offering") +@DiscriminatorValue(value="Service") +@PrimaryKeyJoinColumn(name="id") +public class ServiceOfferingVO extends DiskOfferingVO implements ServiceOffering { + @Column(name="cpu") + private int cpu; + + @Column(name="speed") + private int speed; + + @Column(name="ram_size") + private int ramSize; + + @Column(name="nw_rate") + private int rateMbps; + + @Column(name="mc_rate") + private int multicastRateMbps; + + @Column(name="ha_enabled") + private boolean offerHA; + + @Column(name="guest_ip_type") + @Enumerated(EnumType.STRING) + private GuestIpType guestIpType; + + protected ServiceOfferingVO() { + super(); + } + + public ServiceOfferingVO(String name, int cpu, int ramSize, int speed, int rateMbps, int multicastRateMbps, boolean offerHA, String displayText, GuestIpType guestIpType, boolean useLocalStorage, boolean recreatable, String tags) { + super(name, displayText, false, tags, recreatable, useLocalStorage); + this.cpu = cpu; + this.ramSize = ramSize; + this.speed = speed; + this.rateMbps = rateMbps; + this.multicastRateMbps = multicastRateMbps; + this.offerHA = offerHA; + this.guestIpType = guestIpType; + } + + @Override + public boolean getOfferHA() { + return offerHA; + } + + public void setOfferHA(boolean offerHA) { + this.offerHA = offerHA; + } + + @Override + @Transient + public String[] getTagsArray() { + String tags = getTags(); + if (tags == null || tags.length() == 0) { + return new String[0]; + } + + return tags.split(","); + } + + @Override + public int getCpu() { + return cpu; + } + + public void setCpu(int cpu) { + this.cpu = cpu; + } + + public void setSpeed(int speed) { + this.speed = speed; + } + + public void setRamSize(int ramSize) { + this.ramSize = ramSize; + } + + @Override + public int getSpeed() { + return speed; + } + + @Override + public int getRamSize() { + return ramSize; + } + + public void setRateMbps(int rateMbps) { + this.rateMbps = rateMbps; + } + + public int getRateMbps() { + return rateMbps; + } + + public void setMulticastRateMbps(int multicastRateMbps) { + this.multicastRateMbps = multicastRateMbps; + } + + public int getMulticastRateMbps() { + return multicastRateMbps; + } + + public void setGuestIpType(GuestIpType guestIpType) { + this.guestIpType = guestIpType; + } + + public GuestIpType getGuestIpType() { + return guestIpType; + } +} diff --git a/core/src/com/cloud/service/dao/ServiceOfferingDao.java b/core/src/com/cloud/service/dao/ServiceOfferingDao.java new file mode 100644 index 00000000000..3eee286fadb --- /dev/null +++ b/core/src/com/cloud/service/dao/ServiceOfferingDao.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.service.dao; + +import com.cloud.service.ServiceOfferingVO; +import com.cloud.utils.db.GenericDao; + +/* + * Data Access Object for service_offering table + */ +public interface ServiceOfferingDao extends GenericDao { + ServiceOfferingVO findByName(String name); + ServiceOfferingVO persistSystemServiceOffering(ServiceOfferingVO vo); +} diff --git a/core/src/com/cloud/service/dao/ServiceOfferingDaoImpl.java b/core/src/com/cloud/service/dao/ServiceOfferingDaoImpl.java new file mode 100644 index 00000000000..33ef7cc064c --- /dev/null +++ b/core/src/com/cloud/service/dao/ServiceOfferingDaoImpl.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.service.dao; + +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.service.ServiceOfferingVO; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value={ServiceOfferingDao.class}) @DB(txn=false) +public class ServiceOfferingDaoImpl extends GenericDaoBase implements ServiceOfferingDao { + protected static final Logger s_logger = Logger.getLogger(ServiceOfferingDaoImpl.class); + + protected final SearchBuilder UniqueNameSearch; + protected ServiceOfferingDaoImpl() { + super(); + + UniqueNameSearch = createSearchBuilder(); + UniqueNameSearch.and("name", UniqueNameSearch.entity().getUniqueName(), SearchCriteria.Op.EQ); + UniqueNameSearch.and("removed", UniqueNameSearch.entity().getRemoved(), SearchCriteria.Op.NNULL); + UniqueNameSearch.done(); + } + + @Override + public ServiceOfferingVO findByName(String name) { + SearchCriteria sc = UniqueNameSearch.create(); + sc.setParameters("name", name); + List vos = searchAll(sc, null, null, false); + if (vos.size() == 0) { + return null; + } + + return vos.get(0); + } + + @Override @DB + public ServiceOfferingVO persistSystemServiceOffering(ServiceOfferingVO offering) { + assert offering.getUniqueName() != null : "how are you going to find this later if you don't set it?"; + ServiceOfferingVO vo = findByName(offering.getUniqueName()); + if (vo != null) { + return vo; + } + Transaction txn = Transaction.currentTxn(); + try { + txn.start(); + vo = persist(offering); + remove(vo.getId()); + txn.commit(); + return vo; + } catch (Exception e) { + // Assume it's conflict on unique name + return findByName(offering.getUniqueName()); + } + } +} diff --git a/core/src/com/cloud/storage/DiskOfferingVO.java b/core/src/com/cloud/storage/DiskOfferingVO.java new file mode 100644 index 00000000000..5b972e817a0 --- /dev/null +++ b/core/src/com/cloud/storage/DiskOfferingVO.java @@ -0,0 +1,239 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import java.util.Date; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.DiscriminatorColumn; +import javax.persistence.DiscriminatorType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.Table; +import javax.persistence.Transient; + +import com.cloud.offering.DiskOffering; +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="disk_offering") +@Inheritance(strategy=InheritanceType.JOINED) +@DiscriminatorColumn(name="type", discriminatorType=DiscriminatorType.STRING, length=32) +public class DiskOfferingVO implements DiskOffering { + public enum Type { + Disk, + Service + }; + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + long id; + + @Column(name="domain_id") + Long domainId; + + @Column(name="unique_name") + private String uniqueName; + + @Column(name="name") + private String name = null; + + @Column(name="display_text") + private String displayText = null; + + @Column(name="disk_size") + long diskSize; + + @Column(name="mirrored") + boolean mirrored; + + @Column(name="tags") + String tags; + + @Column(name="type") + Type type; + + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + @Column(name=GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name="recreatable") + private boolean recreatable; + + @Column(name="use_local_storage") + private boolean useLocalStorage; + + + public DiskOfferingVO() { + } + + public DiskOfferingVO(long domainId, String name, String displayText, long diskSize, String tags) { + this.domainId = domainId; + this.name = name; + this.displayText = displayText; + this.diskSize = diskSize; + this.tags = tags; + this.recreatable = false; + this.type = Type.Disk; + this.useLocalStorage = false; + } + + public DiskOfferingVO(String name, String displayText, boolean mirrored, String tags, boolean recreatable, boolean useLocalStorage) { + this.domainId = null; + this.type = Type.Service; + this.name = name; + this.displayText = displayText; + this.mirrored = mirrored; + this.tags = tags; + this.recreatable = recreatable; + this.useLocalStorage = useLocalStorage; + } + + public long getId() { + return id; + } + + public String getUniqueName() { + return uniqueName; + } + + public boolean getUseLocalStorage() { + return useLocalStorage; + } + + public Long getDomainId() { + return domainId; + } + + public Type getType() { + return type; + } + + public boolean isRecreatable() { + return recreatable; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDisplayText() { + return displayText; + } + public void setDisplayText(String displayText) { + this.displayText = displayText; + } + + public long getDiskSize(){ + return diskSize; + } + + public long getDiskSizeInBytes() { + return diskSize * 1024 * 1024; + } + + public void setDiskSize(long diskSize) { + this.diskSize = diskSize; + } + + public boolean isMirrored() { + return mirrored; + } + public void setMirrored(boolean mirrored) { + this.mirrored = mirrored; + } + + public Date getRemoved() { + return removed; + } + + public Date getCreated() { + return created; + } + + protected void setTags(String tags) { + this.tags = tags; + } + + public String getTags() { + return tags; + } + + public void setUniqueName(String name) { + this.uniqueName = name; + } + + @Transient + public String[] getTagsArray() { + String tags = getTags(); + if (tags == null || tags.isEmpty()) { + return new String[0]; + } + + return tags.split(","); + } + + @Transient + public boolean containsTag(String... tags) { + if (this.tags == null) { + return false; + } + + for (String tag : tags) { + if (!this.tags.matches(tag)) { + return false; + } + } + + return true; + } + + @Transient + public void setTagsArray(List newTags) { + if (newTags.isEmpty()) { + setTags(null); + return; + } + + StringBuilder buf = new StringBuilder(); + for (String tag : newTags) { + buf.append(tag).append(","); + } + + buf.delete(buf.length() - 1, buf.length()); + + setTags(buf.toString()); + } +} diff --git a/core/src/com/cloud/storage/DiskTemplateVO.java b/core/src/com/cloud/storage/DiskTemplateVO.java new file mode 100755 index 00000000000..8685ecf221a --- /dev/null +++ b/core/src/com/cloud/storage/DiskTemplateVO.java @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.utils.db.GenericDao; + +/** + * @author ahuang + * + */ +@Entity +@Table(name="disk_template_ref") +public class DiskTemplateVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + Long id; + + @Column(name="description") + String description; + + @Column(name="host") + String host; + + @Column(name="path") + String path; + + @Column(name="size") + long size; + + @Column(name="type") + String type; + + @Column(name=GenericDao.CREATED_COLUMN) + Date created; + + @Column(name=GenericDao.REMOVED_COLUMN) + Date removed; + + public DiskTemplateVO(Long id, String description, String path, long size, String type) { + this.id = id; + this.description = description; + this.path = path; + this.size = size; + this.type = type; + } + + public Long getId() { + return id; + } + + public String getDescription() { + return description; + } + + public String getHost() { + return host; + } + + public String getPath() { + return path; + } + + public long getSize() { + return size; + } + + public String getType() { + return type; + } + + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + protected DiskTemplateVO() { + } +} diff --git a/core/src/com/cloud/storage/FileSystemStorageResource.java b/core/src/com/cloud/storage/FileSystemStorageResource.java new file mode 100644 index 00000000000..4df2f409583 --- /dev/null +++ b/core/src/com/cloud/storage/FileSystemStorageResource.java @@ -0,0 +1,583 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.io.File; +import java.net.InetAddress; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.ModifyStoragePoolAnswer; +import com.cloud.agent.api.ModifyStoragePoolCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.agent.api.StartupStorageCommand; +import com.cloud.agent.api.StoragePoolInfo; +import com.cloud.agent.api.storage.CreatePrivateTemplateAnswer; +import com.cloud.agent.api.storage.CreatePrivateTemplateCommand; +import com.cloud.agent.api.storage.DestroyCommand; +import com.cloud.agent.api.storage.UpgradeDiskAnswer; +import com.cloud.agent.api.storage.UpgradeDiskCommand; +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Volume.StorageResourceType; +import com.cloud.storage.template.TemplateInfo; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NfsUtils; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; + +public abstract class FileSystemStorageResource extends StorageResource { + protected static final Logger s_logger = Logger.getLogger(FileSystemStorageResource.class); + protected String _templateRootDir; + protected String _poolName; + protected String _poolUuid; + protected String _localStoragePath; + + @Override + public boolean existPath(String path) { + if (path == null) { + return false; + } + final Script cmd = new Script("ls", _timeout, s_logger); + cmd.add(File.separator + path); + + final String result = cmd.execute(); + if (result == null) { + return true; + } + + if (result == Script.ERR_TIMEOUT) { + throw new CloudRuntimeException("Script timed out"); + } + + return !result.contains("No such file or directory"); + } + + @Override + public String createPath(final String createPath) { + final Script cmd = new Script("mkdir", _timeout, s_logger); + cmd.add("-p", File.separator + createPath); + + return cmd.execute(); + } + + @Override + protected void fillNetworkInformation(StartupCommand cmd) { + super.fillNetworkInformation(cmd); + cmd.setIqn(null); + } + + + + @Override + protected long getUsedSize() { + return getUsedSize(_rootDir); + } + + + protected long getUsedSize(String poolPath) { + poolPath = getPoolPath(poolPath); + if (poolPath == null) { + return 0; + } + Script command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + command.add("df -Ph " + poolPath + " | grep -v Used | awk '{print $3}' "); + + final OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + if (command.execute(parser) != null) { + return -1; + } + return convertFilesystemSize(parser.getLine()); + } + + private String getPoolPath(String poolPath) { + if (!existPath(poolPath)) { + poolPath = File.separator + poolPath; + if (!existPath(poolPath)) + return null; + } + return poolPath; + } + + protected long getTotalSize(String poolPath) { + poolPath = getPoolPath(poolPath); + if (poolPath == null) { + return 0; + } + Script command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + command.add("df -Ph " + poolPath + " | grep -v Size | awk '{print $2}' "); + + final OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + if (command.execute(parser) != null) { + return -1; + } + return convertFilesystemSize(parser.getLine()); + } + + @Override + protected long getTotalSize() { + return getTotalSize(_rootDir); + } + + @Override + protected String createTrashDir(String imagePath, StringBuilder path) { + final int index = imagePath.lastIndexOf(File.separator) + File.separator.length(); + path.append(_trashcanDir); + path.append(imagePath.substring(_parent.length(), index)); + path.append(Long.toHexString(System.currentTimeMillis())); + + final Script cmd = new Script("mkdir", _timeout, s_logger); + cmd.add("-p", path.toString()); + + final String result = cmd.execute(); + if (result != null) { + return result; + } + + path.append(File.separator).append(imagePath.substring(index)); + return null; + } + + @Override + protected String destroy(String imagePath) { + final StringBuilder trashPath = new StringBuilder(); + String result = createTrashDir(imagePath, trashPath); + if (result != null) { + return result; + } + + final Script cmd = new Script("mv", _timeout, s_logger); + cmd.add(imagePath); + cmd.add(trashPath.toString()); + result = cmd.execute(); + if (result != null) { + return result; + } + + s_logger.warn("Path " + imagePath + " has been moved to " + trashPath.toString()); + + cleanUpEmptyParents(imagePath); + return null; + } + + @Override + protected void cleanUpEmptyParents(String imagePath) { + imagePath = imagePath.substring(0, imagePath.lastIndexOf(File.separator)); + String destroyPath = null; + while (imagePath.length() > _parent.length() && !hasChildren(imagePath)) { + destroyPath = imagePath; + imagePath = imagePath.substring(0, imagePath.lastIndexOf(File.separator)); + } + + if (destroyPath != null) { + final Script cmd = new Script("rm", _timeout, s_logger); + cmd.add("-rf", destroyPath); + cmd.execute(); + } + } + + @Override + protected Answer execute(DestroyCommand cmd) { + VolumeTO volume = cmd.getVolume(); + String result = null; + + result = delete(volume.getPath()); + return new Answer(cmd, result == null, result); + } + + private String delete(String image) { + final Script cmd = new Script(_delvmPath, _timeout, s_logger); + cmd.add("-i", image); + + + final String result = cmd.execute(); + if (result != null) { + return result; + } + + //cleanUpEmptyParents(image); + return null; + } + + + @Override + protected String delete(String imagePath, String extra) { + return delete (imagePath); + } + + protected boolean isSharedNetworkFileSystem(String path) { + if (path.endsWith("/")) { + path = path.substring(0, path.length()-1); + } + Script command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + command.add("mount -t nfs | grep nfs | awk '{print $3}' | grep -x " + path); + + OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + if (command.execute(parser) != null || parser.getLine() == null) { //not NFS client + command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + command.add("grep " + _rootDir + " /etc/exports"); + parser = new OutputInterpreter.OneLineParser(); + if (command.execute(parser) == null && parser.getLine() != null) { + return true; + } + } else if (parser.getLine() != null) { + return true; + } + return false; + } + + + private List getNfsMounts(String path) { + if (path != null && path.endsWith("/")) { + path = path.substring(0, path.length()-1); + } + Script command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + if (path != null) { + command.add("cat /proc/mounts | grep nfs | grep " + path ); + } else { + command.add("cat /proc/mounts | grep nfs | grep -v rpc_pipefs "); + } + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + if (command.execute(parser) != null || parser.getLines() == null) { //not NFS client + return null; + } else { + List result = new ArrayList(); + String[] lines = parser.getLines().split("\\n"); + + for (String line: lines){ + String [] toks = line.split(" "); + if ( toks.length < 4) { + continue; + } + String [] hostpart = toks[0].split(":"); + if (hostpart.length != 2) { + continue; + } + String localPath = toks[1]; + result.add(new String [] {hostpart[0], hostpart[1], localPath}); + + } + return result; + } + } + + public List getNetworkFileSystemServer(String path) { + return getNfsMounts(path); + } + + @Override + protected String create( String templatePath, final String rootdiskFolder, final String userPath, final String datadiskFolder, final String datadiskName, final int datadiskSize, String localPath) { + + s_logger.debug("Creating volumes by cloning " + templatePath); + final Script command = new Script(_createvmPath, _timeout, s_logger); + + command.add("-t", templatePath); + command.add("-i", rootdiskFolder); + command.add("-u", userPath); + if (datadiskSize != 0) { + command.add("-f", datadiskFolder); + command.add("-s", Integer.toString(datadiskSize)); + command.add("-n", datadiskName); + } + + return command.execute(); + } + + @Override + protected UpgradeDiskAnswer execute(UpgradeDiskCommand cmd) { + // TODO Auto-generated method stub + return null; + } + + @Override + public StartupCommand []initialize() { + StartupCommand [] cmds = super.initialize(); + StartupStorageCommand cmd = (StartupStorageCommand)cmds[0]; + + initLocalStorage(cmd); + cmd.setTemplateInfo(new HashMap()); //empty template info + + return new StartupCommand[] {cmd}; + } + + @Override + protected StorageResourceType getStorageResourceType() { + return StorageResourceType.STORAGE_POOL; + } + + protected String mountNfs(String hostAddress, String hostPath, String localPath) { + final OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + Script command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + command.add("mount -t nfs -o acdirmax=0,acdirmin=0 " + hostAddress + ":" + hostPath + " " + localPath); + String result = command.execute(parser); + return result; + } + + protected String umountNfs(String localPath) { + Script command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + command.add("umount " + localPath); + String result = command.execute(); + return result; + } + + protected void initLocalStorage(StartupStorageCommand cmd) { + if (!existPath(_localStoragePath)) { + createPath(_localStoragePath); + } + //setPoolPath(_localStoragePath); + long capacity = getTotalSize(_localStoragePath); + long used = getUsedSize(_localStoragePath); + StoragePoolInfo poolInfo = new StoragePoolInfo( "Local Storage", "file://" + cmd.getPrivateIpAddress() + "/" + _localStoragePath, "localhost", _localStoragePath, _localStoragePath, StoragePoolType.Filesystem, capacity, capacity - used); + cmd.setPoolInfo(poolInfo); + } + + + private Answer setFSStoragePool(ModifyStoragePoolCommand cmd) { + StoragePoolVO pool = cmd.getPool(); + String localPath = pool.getPath(); + File localStorage = new File(localPath); + if (!localStorage.exists()) { + localStorage.mkdir(); + } + /*String result = setPoolPath(localPath); + if (result != null) { + return new Answer(cmd, false, " Failed to create folders"); + }*/ + if (_instance != null) { + localPath = localPath + File.separator + _instance; + } + _poolName = pool.getName(); + long capacity = getTotalSize(localPath); + long used = getUsedSize(localPath); + long available = capacity - used; + Map tInfo = new HashMap(); + ModifyStoragePoolAnswer answer = new ModifyStoragePoolAnswer(cmd, capacity, available, tInfo); + return answer; + } + + @Override + protected Answer execute(ModifyStoragePoolCommand cmd) { + StoragePoolVO pool = cmd.getPool(); + if (pool.getPoolType() == StoragePoolType.Filesystem) { + return setFSStoragePool(cmd); + } + if (cmd.getAdd()) { + String result; + String hostPath = pool.getPath(); + String hostPath2 = pool.getPath(); + if (hostPath.endsWith("/")) { + hostPath2 = hostPath.substring(0, hostPath.length()-1); + } + String localPath = cmd.getLocalPath(); + boolean alreadyMounted = false; + + List shareInfo = getNfsMounts(null); + if (shareInfo != null) { + for (String [] share: shareInfo){ + String host = share[0]; + String path = share[1]; + String path2 = path; + if (path.endsWith("/")) { + path2 = path.substring(0, path.length()-1); + } + if (!path.equals(hostPath) && !path2.equals(hostPath2)){ + continue; + } + if (host.equalsIgnoreCase(pool.getHostAddress())){ + alreadyMounted = true; + localPath = share[2]; + result = null; + break; + } else { + try { + InetAddress currAddr = InetAddress.getByName(host); + InetAddress hostAddr = InetAddress.getByName(pool.getHostAddress()); + if (currAddr.equals(hostAddr)){ + alreadyMounted = true; + result = null; + localPath = share[2]; + break; + } + } catch (UnknownHostException e) { + continue; + } + } + } + } + + String localPath2 = localPath; + if (localPath.endsWith("/")){ + localPath2 = localPath.substring(0,localPath.length()-1); + } + + if (!alreadyMounted){ + Script mkdir = new Script("/bin/bash", _timeout, s_logger); + mkdir.add("-c"); + mkdir.add("mkdir -p " + localPath); + final OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + result = mkdir.execute(parser); + + if (result != null) { + return new Answer(cmd, false, "Failed to create local path: " + result); + } + result = mountNfs(pool.getHostAddress(), pool.getPath(), localPath); + if (result != null) { + return new Answer(cmd, false, " Failed to mount: " + result); + } + } + + /*result = setPoolPath(localPath); + if (result != null) { + return new Answer(cmd, false, " Failed to create folders"); + }*/ + + + if (_instance != null) { + localPath = localPath + File.separator + _instance; + } + _poolName =pool.getName(); + _poolUuid = pool.getUuid(); + long capacity = getTotalSize(localPath); + long used = getUsedSize(localPath); + long available = capacity - used; + Map tInfo = new HashMap(); + ModifyStoragePoolAnswer answer = new ModifyStoragePoolAnswer(cmd, capacity, available, tInfo); + return answer; + } else { + Script command = new Script("/bin/bash", _timeout, s_logger); + command.add("-c"); + command.add("umount " + cmd.getLocalPath()); + final OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + String result = command.execute(parser); + if (result != null) { + return new Answer(cmd, false, " Failed to unmount: " + result); + } + return new Answer(cmd); + } + } + + @Override + protected String create(String templateFolder, String rootdiskFolder, String userPath, String dataPath, String localPath) { + + s_logger.debug("Creating volumes"); + final Script command = new Script(_createvmPath, _timeout, s_logger); + + command.add("-t", templateFolder); + command.add("-i", rootdiskFolder); + command.add("-u", userPath); + + + return command.execute(); + } + + + + protected String mountSecondaryStorage(String tmplMpt, String templatePath, String hostMpt) { + String mountStr = null; + try { + mountStr = NfsUtils.url2Mount(tmplMpt); + } catch (URISyntaxException e) { + s_logger.debug("Is not a valid url" + tmplMpt); + return null; + } + String []tok = mountStr.split(":"); + /*Mount already?*/ + if (!isNfsMounted(tok[0], tok[1], hostMpt)) { + mountNfs(tok[0], tok[1], hostMpt); + } + if (!templatePath.startsWith("/")) + templatePath = hostMpt + "/" + templatePath; + else + templatePath = hostMpt + templatePath; + + return templatePath; + } + + protected boolean isNfsMounted(final String remoteHost, final String remotePath, final String mountPath) { + boolean alreadyMounted = false; + List shareInfo = getNfsMounts(null); + if (shareInfo != null) { + for (String [] share: shareInfo){ + String host = share[0]; + String path = share[1]; + String path2 = path; + String localPath = share[2]; + String localPath2 = localPath; + + if (path.endsWith("/")) { + path2 = path.substring(0, path.length()-1); + } + if (localPath.endsWith("/")) { + localPath2 = localPath.substring(0, localPath.length() -1); + } + if ((!path.equals(remotePath) && !path2.equals(remotePath)) || (!localPath.equals(mountPath) && !localPath2.equals(mountPath)) ){ + continue; + } + + if (host.equalsIgnoreCase(remoteHost)){ + alreadyMounted = true; + break; + } else { + try { + InetAddress currAddr = InetAddress.getByName(host); + InetAddress hostAddr = InetAddress.getByName(remoteHost); + if (currAddr.equals(hostAddr)){ + alreadyMounted = true; + break; + } + } catch (UnknownHostException e) { + continue; + } + } + } + } + return alreadyMounted; + } + + @Override + protected String getDefaultScriptsDir() { + // TODO Auto-generated method stub + return null; + } + + @Override + protected CreatePrivateTemplateAnswer execute(CreatePrivateTemplateCommand cmd) { + CreatePrivateTemplateAnswer answer = super.execute(cmd); + answer.setPath(answer.getPath().replaceFirst(_rootDir, "")); + answer.setPath(answer.getPath().replaceFirst("^/*", "/")); + return answer; + } + + + +} diff --git a/core/src/com/cloud/storage/GuestOS.java b/core/src/com/cloud/storage/GuestOS.java new file mode 100644 index 00000000000..fca72040929 --- /dev/null +++ b/core/src/com/cloud/storage/GuestOS.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +public interface GuestOS { + + Long getId(); + String getName(); + String getDisplayName(); +} diff --git a/core/src/com/cloud/storage/GuestOSCategoryVO.java b/core/src/com/cloud/storage/GuestOSCategoryVO.java new file mode 100644 index 00000000000..56076386d64 --- /dev/null +++ b/core/src/com/cloud/storage/GuestOSCategoryVO.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="guest_os_category") +public class GuestOSCategoryVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + Long id; + + @Column(name="name") + String name; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} \ No newline at end of file diff --git a/core/src/com/cloud/storage/GuestOSVO.java b/core/src/com/cloud/storage/GuestOSVO.java new file mode 100644 index 00000000000..520e5ba0ced --- /dev/null +++ b/core/src/com/cloud/storage/GuestOSVO.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="guest_os") +public class GuestOSVO implements GuestOS { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + Long id; + + @Column(name="category_id") + private long categoryId; + + @Column(name="name") + String name; + + @Column(name="display_name") + String displayName; + + public Long getId() { + return id; + } + + public long getCategoryId() { + return categoryId; + } + + public void setCategoryId(long categoryId) { + this.categoryId = categoryId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } +} diff --git a/core/src/com/cloud/storage/JavaStorageLayer.java b/core/src/com/cloud/storage/JavaStorageLayer.java new file mode 100644 index 00000000000..d8268c44661 --- /dev/null +++ b/core/src/com/cloud/storage/JavaStorageLayer.java @@ -0,0 +1,223 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + + +@Local(value=StorageLayer.class) +public class JavaStorageLayer implements StorageLayer { + + String _name; + + @Override + public boolean cleanup(String path, String rootPath) throws IOException { + assert path.startsWith(rootPath) : path + " does not start with " + rootPath; + + synchronized(path) { + File file = new File(path); + if (!file.delete()) { + return false; + } + int index = -1; + long rootLength = rootPath.length(); + while ((index = path.lastIndexOf(File.separator)) != -1 && path.length() > rootLength) { + file = new File(path.substring(0, index)); + String[] children = file.list(); + if (children != null && children.length > 0) { + break; + } + if (!file.delete()) { + throw new IOException("Unable to delete " + file.getAbsolutePath()); + } + } + return true; + } + } + + @Override + public boolean create(String path, String filename) throws IOException { + synchronized (path.intern()) { + String newFile = path + File.separator + filename; + File file = new File(newFile); + if (file.exists()) { + return true; + } + + return file.createNewFile(); + } + } + + @Override + public boolean delete(String path) { + synchronized(path.intern()) { + File file = new File(path); + return file.delete(); + } + } + + @Override + public boolean exists(String path) { + synchronized(path.intern()) { + File file = new File(path); + return file.exists(); + } + } + + @Override + public long getTotalSpace(String path) { + File file = new File(path); + return file.getTotalSpace(); + } + + @Override + public long getUsableSpace(String path) { + File file = new File(path); + return file.getUsableSpace(); + } + + @Override + public String[] listFiles(String path) { + File file = new File(path); + File[] files = file.listFiles(); + if (files == null) { + return new String[0]; + } + String[] paths = new String[files.length]; + for (int i = 0; i < files.length; i++) { + paths[i] = files[i].getAbsolutePath(); + } + return paths; + } + + @Override + public boolean mkdir(String path) { + synchronized(path.intern()) { + File file = new File(path); + + if (file.exists()) { + return file.isDirectory(); + } + + return (file.mkdirs() && setWorldReadableAndWriteable(file)); + } + } + + @Override + public long getSize(String path) { + File file = new File(path); + return file.length(); + } + + @Override + public boolean mkdirs(String path) { + synchronized(path.intern()) { + File dir = new File(path); + + if (dir.exists()) { + return dir.isDirectory(); + } + + boolean success = true; + List dirPaths = listDirPaths(path); + for (String dirPath : dirPaths) { + dir = new File(dirPath); + if (!dir.exists()) { + success = dir.mkdir() && setWorldReadableAndWriteable(dir); + } + } + + return success; + } + } + + private List listDirPaths(String path) { + String[] dirNames = path.split("/"); + List dirPaths = new ArrayList(); + + String currentPath = ""; + for (int i = 0; i < dirNames.length; i++) { + String currentName = dirNames[i].trim(); + if (!currentName.isEmpty()) { + currentPath += "/" + currentName; + dirPaths.add(currentPath); + } + } + + return dirPaths; + } + + public boolean setWorldReadableAndWriteable(File file) { + return (file.setReadable(true, false) && file.setWritable(true, false)); + } + + @Override + public boolean isDirectory(String path) { + File file = new File(path); + return file.isDirectory(); + } + + @Override + public boolean isFile(String path) { + File file = new File(path); + return file.isFile(); + } + + @Override + public File getFile(String path) { + return new File(path); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + _name = name; + return true; + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } + + @Override + public long getUsedSpace(String path) { + File file = new File(path); + return file.getTotalSpace() - file.getFreeSpace(); + } + + + +} diff --git a/core/src/com/cloud/storage/LaunchPermissionVO.java b/core/src/com/cloud/storage/LaunchPermissionVO.java new file mode 100644 index 00000000000..fb15fa8c9c5 --- /dev/null +++ b/core/src/com/cloud/storage/LaunchPermissionVO.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="launch_permission") +public class LaunchPermissionVO { + @Id + @Column(name="id") + private Long id; + + @Column(name="template_id") + private long templateId; + + @Column(name="account_id") + private long accountId; + + public LaunchPermissionVO() { } + + public LaunchPermissionVO(long templateId, long accountId) { + this.templateId = templateId; + this.accountId = accountId; + } + + public Long getId() { + return id; + } + + public long getTemplateId() { + return templateId; + } + + public long getAccountId() { + return accountId; + } +} diff --git a/core/src/com/cloud/storage/SecondaryStorage.java b/core/src/com/cloud/storage/SecondaryStorage.java new file mode 100644 index 00000000000..82c64bf9225 --- /dev/null +++ b/core/src/com/cloud/storage/SecondaryStorage.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +public interface SecondaryStorage { + + String getBackupPath(); + + String getTemplatePath(); + + String getIsoPath(); + + void createTemplate(); + + void destroyTemplate(); +} diff --git a/core/src/com/cloud/storage/SecondaryStorageLayer.java b/core/src/com/cloud/storage/SecondaryStorageLayer.java new file mode 100644 index 00000000000..7b990e00cc6 --- /dev/null +++ b/core/src/com/cloud/storage/SecondaryStorageLayer.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.net.URI; + +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.utils.component.Adapter; + +public interface SecondaryStorageLayer extends Adapter { + + /** + * Mounts a template + * + * @param poolId the pool to mount it to. + * @param poolUuid the pool's uuid if it is needed. + * @param name unique name to the template. + * @param url url to access the template. + * @param format format of the template. + * @param accountId account id the template belongs to. + * @return a String that unique identifies the reference the template once it is mounted. + */ + String mountTemplate(long poolId, String poolUuid, String name, URI url, ImageFormat format, long accountId); + +} diff --git a/core/src/com/cloud/storage/Snapshot.java b/core/src/com/cloud/storage/Snapshot.java new file mode 100644 index 00000000000..87ae90509f0 --- /dev/null +++ b/core/src/com/cloud/storage/Snapshot.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import java.util.Date; + +public interface Snapshot { + public enum SnapshotType { + MANUAL, + RECURRING, + TEMPLATE; + + public String toString() { + return this.name(); + } + + public boolean equals(String snapshotType) { + return this.toString().equalsIgnoreCase(snapshotType); + } + } + + public enum Status { + Creating, + CreatedOnPrimary, + BackingUp, + BackedUp; + + public String toString() { + return this.name(); + } + + public boolean equals(String status) { + return this.toString().equalsIgnoreCase(status); + } + } + + public static final long MANUAL_POLICY_ID = 1L; + + Long getId(); + long getAccountId(); + long getVolumeId(); + String getPath(); + String getName(); + Date getCreated(); + short getSnapshotType(); + Status getStatus(); +} diff --git a/core/src/com/cloud/storage/SnapshotPolicyRefVO.java b/core/src/com/cloud/storage/SnapshotPolicyRefVO.java new file mode 100644 index 00000000000..ab06e6796f9 --- /dev/null +++ b/core/src/com/cloud/storage/SnapshotPolicyRefVO.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +@Entity +@Table(name="snapshot_policy_ref") +public class SnapshotPolicyRefVO { + + @Column(name="snap_id") + long snapshotId; + + @Column(name="volume_id") + long volumeId; + + @Column(name="policy_id") + long policyId; + + public SnapshotPolicyRefVO() { } + + public SnapshotPolicyRefVO(long snapshotId, long volumeId, long policyId) { + this.snapshotId = snapshotId; + this.volumeId = volumeId; + this.policyId = policyId; + } + + public long getSnapshotId() { + return snapshotId; + } + + public long getVolumeId() { + return snapshotId; + } + + public long getPolicyId() { + return policyId; + } +} diff --git a/core/src/com/cloud/storage/SnapshotPolicyVO.java b/core/src/com/cloud/storage/SnapshotPolicyVO.java new file mode 100644 index 00000000000..27bb2ed04b9 --- /dev/null +++ b/core/src/com/cloud/storage/SnapshotPolicyVO.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="snapshot_policy") +public class SnapshotPolicyVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + Long id; + + @Column(name="volume_id") + long volumeId; + + @Column(name="schedule") + String schedule; + + @Column(name="timezone") + String timezone; + + @Column(name="interval") + private short interval; + + @Column(name="max_snaps") + private int maxSnaps; + + @Column(name="active") + boolean active = false; + + public SnapshotPolicyVO() { } + + public SnapshotPolicyVO(long volumeId, String schedule, String timezone, short interval, int maxSnaps) { + this.volumeId = volumeId; + this.schedule = schedule; + this.timezone = timezone; + this.interval = interval; + this.maxSnaps = maxSnaps; + this.active = true; + } + + public Long getId() { + return id; + } + + public long getVolumeId() { + return volumeId; + } + + public void setSchedule(String schedule) { + this.schedule = schedule; + } + + public String getSchedule() { + return schedule; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } + + public String getTimezone() { + return timezone; + } + + public short getInterval() { + return interval; + } + + public void setMaxSnaps(int maxSnaps) { + this.maxSnaps = maxSnaps; + } + + public int getMaxSnaps() { + return maxSnaps; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } +} diff --git a/core/src/com/cloud/storage/SnapshotScheduleVO.java b/core/src/com/cloud/storage/SnapshotScheduleVO.java new file mode 100644 index 00000000000..b112cd19fa9 --- /dev/null +++ b/core/src/com/cloud/storage/SnapshotScheduleVO.java @@ -0,0 +1,102 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="snapshot_schedule") +public class SnapshotScheduleVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + long id; + + // DB constraint: For a given volume and policyId, there will only be one entry in this table. + @Column(name="volume_id") + long volumeId; + + @Column(name="policy_id") + long policyId; + + @Column(name="scheduled_timestamp") + @Temporal(value=TemporalType.TIMESTAMP) + Date scheduledTimestamp; + + @Column(name="async_job_id") + Long asyncJobId; + + @Column(name="snapshot_id") + Long snapshotId; + + public SnapshotScheduleVO() { } + + public SnapshotScheduleVO(long volumeId, long policyId, Date scheduledTimestamp) { + this.volumeId = volumeId; + this.policyId = policyId; + this.scheduledTimestamp = scheduledTimestamp; + this.snapshotId = null; + this.asyncJobId = null; + } + + public Long getId() { + return id; + } + + public Long getVolumeId() { + return volumeId; + } + + public Long getPolicyId() { + return policyId; + } + + /** + * @return the scheduledTimestamp + */ + public Date getScheduledTimestamp() { + return scheduledTimestamp; + } + + public Long getAsyncJobId() { + return asyncJobId; + } + + public void setAsyncJobId(long asyncJobId) { + this.asyncJobId = asyncJobId; + } + + public Long getSnapshotId() { + return snapshotId; + } + + public void setSnapshotId(Long snapshotId) { + this.snapshotId = snapshotId; + } +} \ No newline at end of file diff --git a/core/src/com/cloud/storage/SnapshotVO.java b/core/src/com/cloud/storage/SnapshotVO.java new file mode 100644 index 00000000000..bce1e8f699b --- /dev/null +++ b/core/src/com/cloud/storage/SnapshotVO.java @@ -0,0 +1,193 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import java.util.Date; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.utils.db.GenericDao; +import com.google.gson.annotations.Expose; + +@Entity +@Table(name="snapshots") +public class SnapshotVO implements Snapshot { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + Long id; + + @Column(name="account_id") + long accountId; + + @Column(name="volume_id") + long volumeId; + + @Expose + @Column(name="path") + String path; + + @Expose + @Column(name="name") + String name; + + @Expose + @Column(name="status", updatable = true, nullable=false) + @Enumerated(value=EnumType.STRING) + private Status status; + + @Column(name="snapshot_type") + short snapshotType; + + @Column(name="type_description") + String typeDescription; + + @Column(name=GenericDao.CREATED_COLUMN) + Date created; + + @Column(name=GenericDao.REMOVED_COLUMN) + Date removed; + + @Column(name="backup_snap_id") + String backupSnapshotId; + + @Column(name="prev_snap_id") + long prevSnapshotId; + + public SnapshotVO() { } + + public SnapshotVO(long accountId, long volumeId, String path, String name, short snapshotType, String typeDescription) { + this.accountId = accountId; + this.volumeId = volumeId; + this.path = path; + this.name = name; + this.snapshotType = snapshotType; + this.typeDescription = typeDescription; + this.status = Status.Creating; + this.prevSnapshotId = 0; + } + + @Override + public Long getId() { + return id; + } + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public long getVolumeId() { + return volumeId; + } + + @Override + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + @Override + public String getName() { + return name; + } + + @Override + public short getSnapshotType() { + return snapshotType; + } + public void setSnapshotType(short snapshotType) { + this.snapshotType = snapshotType; + } + + public String getTypeDescription() { + return typeDescription; + } + public void setTypeDescription(String typeDescription) { + this.typeDescription = typeDescription; + } + + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + @Override + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getBackupSnapshotId(){ + return backupSnapshotId; + } + + public long getPrevSnapshotId(){ + return prevSnapshotId; + } + + public void setBackupSnapshotId(String backUpSnapshotId){ + this.backupSnapshotId = backUpSnapshotId; + } + + public void setPrevSnapshotId(long prevSnapshotId){ + this.prevSnapshotId = prevSnapshotId; + } + + public static SnapshotType getSnapshotType(List policyIds) { + assert policyIds != null && !policyIds.isEmpty(); + SnapshotType snapshotType = SnapshotType.RECURRING; + if (policyIds.contains(MANUAL_POLICY_ID)) { + snapshotType = SnapshotType.MANUAL; + } + return snapshotType; + } + + public static SnapshotType getSnapshotType(String snapshotType) { + if (SnapshotType.MANUAL.equals(snapshotType)) { + return SnapshotType.MANUAL; + } + if (SnapshotType.RECURRING.equals(snapshotType)) { + return SnapshotType.RECURRING; + } + if (SnapshotType.TEMPLATE.equals(snapshotType)) { + return SnapshotType.TEMPLATE; + } + return null; + } +} diff --git a/core/src/com/cloud/storage/StorageLayer.java b/core/src/com/cloud/storage/StorageLayer.java new file mode 100644 index 00000000000..532a9bcf6b9 --- /dev/null +++ b/core/src/com/cloud/storage/StorageLayer.java @@ -0,0 +1,150 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.io.File; +import java.io.IOException; + +import com.cloud.utils.component.Manager; + + +/** + * StorageLayer is an independence layer for + * interfacing with the file system storage. + * + * All implementations must guarantee the following things: + * 1. Proper synchronization between threads. + * + * + */ +public interface StorageLayer extends Manager { + public final static String InstanceConfigKey = "storage.layer.instance"; + public final static String ClassConfigKey = "storage.layer.implementation"; + + /** + * Returns the size of the file. + * @param path path to the file to get the size. + * @return size of the file. + */ + long getSize(String path); + + /** + * Is this path a directory? + * @param path path to check. + * @return true if it is a directory; false otherwise. + */ + boolean isDirectory(String path); + + /** + * Is this path a file? + * @param path path to check. + * @return true if it is a file; false otherwise. + */ + boolean isFile(String path); + + /** + * creates the directory. All parent directories have to already exists. + * @param path path to make. + * @return true if created; false if not. + */ + boolean mkdir(String path); + + /** + * Creates the entire path. + * @param path path to create. + * @return true if created; false if not. + */ + boolean mkdirs(String path); + + /** + * Does this path exists? + * @param path directory or file to check if it exists. + * @return true if exists; false if not. + */ + boolean exists(String path); + + /** + * list all the files in a certain path. + * @param path directory that the file exists in. + * @return list of files that exists under this path. + */ + String[] listFiles(String path); + + /** + * Get the total disk size in bytes. + * @param path path + * @return disk size if path given is a disk; -1 if not. + */ + long getTotalSpace(String path); + + /** + * Get the total available disk size in bytes. + * @param path path to the disk. + * @return disk size if path given is a disk; -1 if not. + */ + long getUsedSpace(String path); + + /** + * Get the total available disk size in bytes. + * @param path path to the disk. + * @return disk size if path given is a disk; -1 if not. + */ + long getUsableSpace(String path); + + /** + * delete the path + * @param path to delete. + * @return true if deleted; false if not. + */ + boolean delete(String path); + + /** + * creates a file on this path. + * @param path directory to create the file in. + * @param filename file to create. + * @return true if created; false if not. + * @throws IOException if create has problems. + */ + boolean create(String path, String filename) throws IOException; + + /** + * clean up the path. This method will delete the parent paths if the parent + * paths do not contain children. If the original path cannot be deleted, + * this method returns false. If the parent cannot be deleted but does not + * have any children, this method throws IOException. + * @param path path to be cleaned up. + * @param rootPath delete up to this path. + * @return true if the path is deleted and false if it is not. + * @throws IOException if the parent has no children but delete failed. + */ + boolean cleanup(String path, String rootPath) throws IOException; + + /** + * Retrieves the actual file object. + * @param path path to the file. + * @return File object representing the file. + */ + File getFile(String path); + + /** + * Sets permissions for a file to be world readable and writeable + * @param file + * @return true if the file was set to be both world readable and writeable + */ + boolean setWorldReadableAndWriteable(File file); +} diff --git a/core/src/com/cloud/storage/StorageManager.java b/core/src/com/cloud/storage/StorageManager.java new file mode 100644 index 00000000000..7b7ed9c1aef --- /dev/null +++ b/core/src/com/cloud/storage/StorageManager.java @@ -0,0 +1,291 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.net.URI; +import java.net.UnknownHostException; +import java.util.List; +import java.util.Map; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.HostPodVO; +import com.cloud.exception.InternalErrorException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceInUseException; +import com.cloud.exception.StorageUnavailableException; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; +import com.cloud.utils.component.Manager; +import com.cloud.utils.exception.ExecutionException; +import com.cloud.vm.VMInstanceVO; + +public interface StorageManager extends Manager { + /** + * Calls the storage agent and makes the volumes sharable with this host. + * + * @param vm vm that owns the volumes + * @param vols volumes to share + * @param host host to share the volumes to. + * @param cancelPrevious cancel the previous shares? + * @return true if works. + * + * @throws StorageUnavailableException if the storage server is unavailable. + */ + boolean share(VMInstanceVO vm, List vols, HostVO host, boolean cancelPrevious) throws StorageUnavailableException; + + List prepare(VMInstanceVO vm, HostVO host); + + /** + * Calls the storage server to unshare volumes to the host. + * + * @param vm vm that owns the volumes. + * @param vols volumes to remove from share. + * @param host host to unshare the volumes to. + * @return true if it worked; false if not. + */ + boolean unshare(VMInstanceVO vm, List vols, HostVO host); + + /** + * unshares the storage volumes of a certain vm to the host. + * + * @param vm vm to unshare. + * @param host host. + * @return List if succeeded. null if not. + */ + List unshare(VMInstanceVO vm, HostVO host); + + /** + * destroy the storage volumes of a certain vm. + * + * @param vm vm to destroy. + * @param vols volumes to remove from storage pool + */ + void destroy(VMInstanceVO vm, List vols); + + /** + * Creates volumes for a particular VM. + * @param account account to create volumes for. + * @param vm vm to create the volumes for. + * @param template template the root volume is based on. + * @param dc datacenter to put this. + * @param pod pod to put this. + * @param offering service offering of the vm. + * @param diskOffering disk offering of the vm. + * @param avoids storage pools to avoid. + * @param size : size of the volume if defined + * @return List of VolumeVO + */ + List create(Account account, VMInstanceVO vm, VMTemplateVO template, DataCenterVO dc, HostPodVO pod, ServiceOfferingVO offering, DiskOfferingVO diskOffering, long size) throws StorageUnavailableException, ExecutionException; + + /** + * Create StoragePool based on uri + * + * @param zoneId + * @param podId + * @param poolName + * @param uriString + * @return + * @throws ResourceInUseException, IllegalArgumentException + * @throws ResourceAllocationException + */ + StoragePoolVO createPool(long zoneId, Long podId, Long clusterId, String poolName, URI uri, String tags, Map details) throws ResourceInUseException, IllegalArgumentException, UnknownHostException, ResourceAllocationException; + + /** + * Get the storage ip address to connect to. + * @param vm vm to run. + * @param host host to run it on. + * @param storage storage that contains the vm. + * @return ip address if it can be determined. null if not. + */ + String chooseStorageIp(VMInstanceVO vm, Host host, Host storage); + + boolean canVmRestartOnAnotherServer(long vmId); + + /** Returns the absolute path of the specified ISO + * @param templateId - the ID of the template that represents the ISO + * @param datacenterId + * @return absolute ISO path + */ + public String getAbsoluteIsoPath(long templateId, long dataCenterId); + + /** + * Returns the URL of the secondary storage host + * @param zoneId + * @return URL + */ + public String getSecondaryStorageURL(long zoneId); + + /** + * Returns a comma separated list of tags for the specified storage pool + * @param poolId + * @return comma separated list of tags + */ + public String getStoragePoolTags(long poolId); + + /** + * Returns the secondary storage host + * @param zoneId + * @return secondary storage host + */ + public HostVO getSecondaryStorageHost(long zoneId); + + /** + * Create the volumes for a user VM based on service offering in a particular data center + * + * @return true if successful + */ + public long createUserVM(Account account, VMInstanceVO vm, + VMTemplateVO template, DataCenterVO dc, HostPodVO pod, + ServiceOfferingVO offering, DiskOfferingVO diskOffering, + List avoids, long size); + + /** + * This method sends the given command on all the hosts in the primary storage pool given until is succeeds on any one. + * If the command doesn't succeed on any, it return null. All exceptions are swallowed. Any errors are expected be be in + * answer.getDetails(), if it's not null. + * @param poolId The primary storage pool. The cmd uses this for some reason. + * @param cmd Any arbitrary command which needs access to the volumes on the given storage pool. + * @param basicErrMsg The cmd specific error msg to spew out in case of any exception. + * @return The answer for that command, could be success or failure. + */ + Answer sendToHostsOnStoragePool(Long poolId, Command cmd, String basicErrMsg); + Answer sendToHostsOnStoragePool(Long poolId, Command cmd, String basicErrMsg, int retriesPerHost, int pauseBeforeRetry, boolean shouldBeSnapshotCapable, Long vmId ); + + + /** + * Add a pool to a host + * @param hostId + * @param pool + */ + boolean addPoolToHost(long hostId, StoragePoolVO pool); + + /** + * Moves a volume from its current storage pool to a storage pool with enough capacity in the specified zone, pod, or cluster + * @param volume + * @param destPoolDcId + * @param destPoolPodId + * @param destPoolClusterId + * @return VolumeVO + */ + VolumeVO moveVolume(VolumeVO volume, long destPoolDcId, Long destPoolPodId, Long destPoolClusterId) throws InternalErrorException; + + /** + * Creates a new volume in a pool in the specified zone + * @param accountId + * @param userId + * @param name + * @param dc + * @param diskOffering + * @param size + * @return VolumeVO + */ + VolumeVO createVolume(long accountId, long userId, String name, DataCenterVO dc, DiskOfferingVO diskOffering, long startEventId, long size); + + /** + * Marks the specified volume as destroyed in the management server database. The expunge thread will delete the volume from its storage pool. + * @param volume + */ + void destroyVolume(VolumeVO volume); + + /** Create capacity entries in the op capacity table + * @param storagePool + */ + public void createCapacityEntry(StoragePoolVO storagePool); + + /** + * Checks that the volume is stored on a shared storage pool + * @param volume + * @return true if the volume is on a shared storage pool, false otherwise + */ + boolean volumeOnSharedStoragePool(VolumeVO volume); + + Answer[] sendToPool(StoragePoolVO pool, Command[] cmds, boolean stopOnError); + + Answer sendToPool(StoragePoolVO pool, Command cmd); + + /** + * Checks that one of the following is true: + * 1. The volume is not attached to any VM + * 2. The volume is attached to a VM that is running on a host with the KVM hypervisor, and the VM is stopped + * 3. The volume is attached to a VM that is running on a host with the XenServer hypervisor (the VM can be stopped or running) + * @return true if one of the above conditions is true + */ + boolean volumeInactive(VolumeVO volume); + + String getVmNameOnVolume(VolumeVO volume); + + List> isStoredOn(VMInstanceVO vm); + + /** + * Checks if a host has running VMs that are using its local storage pool. + * @return true if local storage is active on the host + */ + boolean isLocalStorageActiveOnHost(HostVO host); + + /** + * Cleans up storage pools by removing unused templates. + * @param recurring - true if this cleanup is part of a recurring garbage collection thread + */ + void cleanupStorage(boolean recurring); + + /** + * Delete the storage pool + * @param id -- id associated + */ + boolean deletePool(long id); + + /** + * Updates a storage pool. + * @param poolId ID of the storage pool to be updated + * @param tags Tags that will be added to the storage pool + */ + StoragePoolVO updateStoragePool(long poolId, String tags) throws IllegalArgumentException; + + /** + * Find all of the storage pools needed for this vm. + * + * @param vmId id of the vm. + * @return List of StoragePoolVO + */ + List getStoragePoolsForVm(long vmId); + + String getPrimaryStorageNameLabel(VolumeVO volume); + + /** + * Creates a volume from the specified snapshot. A new volume is returned which is not attached to any VM Instance + */ + VolumeVO createVolumeFromSnapshot(long userId, long accountId, long snapshotId, String volumeName, long startEventId); + + /** + * Enable maintenance for primary storage + * @return + */ + public boolean preparePrimaryStorageForMaintenance(long primaryStorageId, long userId); + + /** + * Complete maintenance for primary storage + * @return + */ + public boolean cancelPrimaryStorageForMaintenance(long primaryStorageId, long userId); +} diff --git a/core/src/com/cloud/storage/StoragePool.java b/core/src/com/cloud/storage/StoragePool.java new file mode 100644 index 00000000000..2c06aba7628 --- /dev/null +++ b/core/src/com/cloud/storage/StoragePool.java @@ -0,0 +1,104 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.util.Date; + +import com.cloud.host.Status; +import com.cloud.storage.Storage.StoragePoolType; + +/** + * @author chiradeep + * + */ + +public interface StoragePool { + + /** + * @return id of the pool. + */ + long getId(); + + /** + * @return name of the pool. + */ + String getName(); + + /*** + * + * @return unique identifier + */ + String getUuid(); + + /** + * @return the type of pool. + */ + StoragePoolType getPoolType(); + + /** + * @return the date the pool first registered + */ + Date getCreated(); + + /** + * @return the last time the state of this pool was modified. + */ + Date getUpdateTime(); + + + /** + * @return availability zone. + */ + long getDataCenterId(); + + /** + * @return capacity of storage poolin bytes + */ + long getCapacityBytes(); + + + /** + * @return available storage in bytes + */ + long getAvailableBytes(); + + /** + * @return the fqdn or ip address of the storage host + */ + String getHostAddress(); + + /** + * @return the filesystem path of the pool on the storage host (server) + */ + String getPath(); + + /** + * @return the storage pool represents a shared storage resource + */ + boolean isShared(); + + /** + * @return the storage pool represents a local storage resource + */ + boolean isLocal(); + + /** + * @return the storage pool status + */ + Status getStatus(); +} diff --git a/core/src/com/cloud/storage/StoragePoolDetailVO.java b/core/src/com/cloud/storage/StoragePoolDetailVO.java new file mode 100644 index 00000000000..e7f6793ba86 --- /dev/null +++ b/core/src/com/cloud/storage/StoragePoolDetailVO.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="storage_pool_details") +public class StoragePoolDetailVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + long id; + + @Column(name="pool_id") + long poolId; + + @Column(name="name") + String name; + + @Column(name="value") + String value; + + public StoragePoolDetailVO(long poolId, String name, String value) { + this.poolId = poolId; + this.name = name; + this.value = value; + } + + public long getId() { + return id; + } + + public long getPoolId() { + return poolId; + } + + public void setPoolId(long poolId) { + this.poolId = poolId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + protected StoragePoolDetailVO() { + } +} diff --git a/core/src/com/cloud/storage/StoragePoolDiscoverer.java b/core/src/com/cloud/storage/StoragePoolDiscoverer.java new file mode 100644 index 00000000000..d45cc7ac972 --- /dev/null +++ b/core/src/com/cloud/storage/StoragePoolDiscoverer.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.net.URI; +import java.util.Map; + +import com.cloud.exception.DiscoveryException; +import com.cloud.utils.component.Adapter; + +/** + * Discoverer to find new Storage Pools. + */ +public interface StoragePoolDiscoverer extends Adapter { + + Map> find(long dcId, Long podId, URI uri, Map details) throws DiscoveryException; + + Map> find(long dcId, Long podId, URI uri, Map details, String username, String password) throws DiscoveryException; +} diff --git a/core/src/com/cloud/storage/StoragePoolHostAssoc.java b/core/src/com/cloud/storage/StoragePoolHostAssoc.java new file mode 100644 index 00000000000..5e8a297cf5f --- /dev/null +++ b/core/src/com/cloud/storage/StoragePoolHostAssoc.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.util.Date; + +/** + * @author chiradeep + * + */ +public interface StoragePoolHostAssoc { + + long getHostId(); + + long getPoolId(); + + String getLocalPath(); + + Date getCreated(); + + Date getLastUpdated(); + +} diff --git a/core/src/com/cloud/storage/StoragePoolHostVO.java b/core/src/com/cloud/storage/StoragePoolHostVO.java new file mode 100644 index 00000000000..62ac4b12578 --- /dev/null +++ b/core/src/com/cloud/storage/StoragePoolHostVO.java @@ -0,0 +1,105 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.utils.db.GenericDaoBase; + +/** + * Join table for storage pools and hosts + * @author chiradeep + * + */ +@Entity +@Table(name="storage_pool_host_ref") +public class StoragePoolHostVO implements StoragePoolHostAssoc { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + Long id; + + @Column(name="pool_id") + private long poolId; + + @Column(name="host_id") + private long hostId; + + @Column(name="local_path") + private String localPath; + + @Column(name=GenericDaoBase.CREATED_COLUMN) + private Date created = null; + + @Column(name="last_updated") + @Temporal(value=TemporalType.TIMESTAMP) + private Date lastUpdated = null; + + + public StoragePoolHostVO() { + super(); + } + + + public StoragePoolHostVO(long poolId, long hostId, String localPath) { + this.poolId = poolId; + this.hostId = hostId; + this.localPath = localPath; + } + + + @Override + public long getHostId() { + return hostId; + } + + + @Override + public String getLocalPath() { + return localPath; + } + + @Override + public long getPoolId() { + return poolId; + } + + @Override + public Date getCreated() { + return created; + } + + @Override + public Date getLastUpdated() { + return lastUpdated; + } + + + public void setLocalPath(String localPath) { + this.localPath = localPath; + } + +} diff --git a/core/src/com/cloud/storage/StoragePoolVO.java b/core/src/com/cloud/storage/StoragePoolVO.java new file mode 100644 index 00000000000..41cabb559b7 --- /dev/null +++ b/core/src/com/cloud/storage/StoragePoolVO.java @@ -0,0 +1,271 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.TableGenerator; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.Transient; + +import com.cloud.host.Status; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.utils.db.GenericDao; + +/** + * @author chiradeep + * + */ +@Entity +@Table(name="storage_pool") +public class StoragePoolVO implements StoragePool { + @Id + @TableGenerator(name="storage_pool_sq", table="sequence", pkColumnName="name", valueColumnName="value", pkColumnValue="storage_pool_seq", allocationSize=1) + @Column(name="id", updatable=false, nullable = false) + private long id; + + @Column(name="name", updatable=false, nullable=false, length=255) + private String name = null; + + @Column(name="uuid", updatable=false, nullable=false, length=255) + private String uuid = null; + + @Column(name="pool_type", updatable=false, nullable=false, length=32) + @Enumerated(value=EnumType.STRING) + private StoragePoolType poolType; + + @Column(name=GenericDao.CREATED_COLUMN) + Date created; + + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + @Column(name="update_time", updatable=true) + @Temporal(value=TemporalType.TIMESTAMP) + private Date updateTime; + + @Column(name="data_center_id", updatable=true, nullable=false) + private long dataCenterId; + + @Column(name="pod_id", updatable=true) + private Long podId; + + @Column(name="available_bytes", updatable=true, nullable=true) + private long availableBytes; + + @Column(name="capacity_bytes", updatable=true, nullable=true) + private long capacityBytes; + + @Column(name="status", updatable=true, nullable=false) + @Enumerated(value=EnumType.STRING) + private Status status; + + public long getId() { + return id; + } + + @Override + public Status getStatus() { + return status; + } + + public StoragePoolVO() { + // TODO Auto-generated constructor stub + } + + public String getName() { + return name; + } + + public String getUuid() { + return uuid; + } + + public StoragePoolType getPoolType() { + return poolType; + } + + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + public Date getUpdateTime() { + return updateTime; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public long getAvailableBytes() { + return availableBytes; + } + + public long getCapacityBytes() { + return capacityBytes; + } + + public void setAvailableBytes(long available) { + availableBytes = available; + } + + public void setCapacityBytes(long capacity) { + capacityBytes = capacity; + } + + @Column(name="host_address") + private String hostAddress; + + @Column(name="path") + private String path; + + @Column(name="port") + private int port; + + @Column(name="cluster_id") + private Long clusterId; + + + public Long getClusterId() { + return clusterId; + } + + public void setClusterId(Long clusterId) { + this.clusterId = clusterId; + } + + public String getHostAddress() { + return hostAddress; + } + + public String getPath() { + return path; + } + + public StoragePoolVO(long poolId, String name, String uuid, StoragePoolType type, + long dataCenterId, Long podId, long availableBytes, long capacityBytes, String hostAddress, int port, String hostPath) { + this.name = name; + this.id = poolId; + this.uuid = uuid; + this.poolType = type; + this.dataCenterId = dataCenterId; + this.availableBytes = availableBytes; + this.capacityBytes = capacityBytes; + this.hostAddress = hostAddress; + this.path = hostPath; + this.port = port; + this.podId = podId; + } + + public StoragePoolVO(StoragePoolType type, String hostAddress, int port, String path) { + this.poolType = type; + this.hostAddress = hostAddress; + this.port = port; + this.path = path; + } + + public void setStatus(Status status) + { + this.status = status; + } + + public void setId(long id) { + this.id = id; + } + + public void setDataCenterId(long dcId) { + this.dataCenterId = dcId; + } + + public void setPodId(Long podId) { + this.podId = podId; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public int getPort() { + return port; + } + + public boolean isShared() { + return poolType.isShared(); + } + + public boolean isLocal() { + return !poolType.isShared(); + } + + @Transient + public String toUri() { + /* + URI uri = new URI(); + try { + if (type == StoragePoolType.Filesystem) { + uri.setScheme("file"); + } else if (type == StoragePoolType.NetworkFilesystem) { + uri.setScheme("nfs"); + } else if (type == StoragePoolType.IscsiLUN) { + } + } catch (MalformedURIException e) { + throw new VmopsRuntimeException("Unable to form the uri " + id); + } + return uri.toString(); + */ + return null; + } + + public Long getPodId() { + return podId; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StoragePoolVO) || obj == null) { + return false; + } + StoragePoolVO that = (StoragePoolVO)obj; + return this.id == that.id; + } + + @Override + public int hashCode() { + return new Long(id).hashCode(); + } + + @Override + public String toString() { + return new StringBuilder("Pool[").append(id).append("|").append(poolType).append("]").toString(); + } +} diff --git a/core/src/com/cloud/storage/StorageResource.java b/core/src/com/cloud/storage/StorageResource.java new file mode 100755 index 00000000000..6ca2dd80cc7 --- /dev/null +++ b/core/src/com/cloud/storage/StorageResource.java @@ -0,0 +1,591 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.BackupSnapshotAnswer; +import com.cloud.agent.api.BackupSnapshotCommand; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.GetFileStatsAnswer; +import com.cloud.agent.api.GetFileStatsCommand; +import com.cloud.agent.api.GetStorageStatsAnswer; +import com.cloud.agent.api.GetStorageStatsCommand; +import com.cloud.agent.api.ManageSnapshotAnswer; +import com.cloud.agent.api.ManageSnapshotCommand; +import com.cloud.agent.api.ModifyStoragePoolCommand; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.PingStorageCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.agent.api.StartupStorageCommand; +import com.cloud.agent.api.storage.CreatePrivateTemplateAnswer; +import com.cloud.agent.api.storage.CreatePrivateTemplateCommand; +import com.cloud.agent.api.storage.DestroyCommand; +import com.cloud.agent.api.storage.DownloadCommand; +import com.cloud.agent.api.storage.PrimaryStorageDownloadCommand; +import com.cloud.agent.api.storage.ShareAnswer; +import com.cloud.agent.api.storage.ShareCommand; +import com.cloud.agent.api.storage.UpgradeDiskAnswer; +import com.cloud.agent.api.storage.UpgradeDiskCommand; +import com.cloud.host.Host; +import com.cloud.resource.ServerResource; +import com.cloud.resource.ServerResourceBase; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.template.DownloadManager; +import com.cloud.storage.template.TemplateInfo; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; + +/** + * StorageResource represents the storage server. It executes commands + * against the storage server. + * + * @config + * {@table + * || Param Name | Description | Values | Default || + * || pool | name of the pool to use | String | tank || + * || parent | parent path to all of the templates and the trashcan | Path | [pool]/vmops || + * || scripts.dir | directory to the scripts | Path | ./scripts || + * || scripts.timeout | timeout value to use when executing scripts | seconds | 120s || + * || public.templates.root.dir | directory where public templates reside | Path | [pool]/volumes/demo/template/public/os || + * || private.templates.root.dir | directory where private templates reside | Path | [pool]/volumes/template/demo/private || + * || templates.download.dir | directory where templates are downloaded prior to being installed | Path | [pool]/volumes/demo/template/download || + * || install.timeout.pergig | timeout for the template creation script per downloaded gigabyte | seconds | 900s || + * || install.numthreads | number of concurrent install threads | number | 3 || + * } + */ +public abstract class StorageResource extends ServerResourceBase implements ServerResource { + protected static final Logger s_logger = Logger.getLogger(StorageResource.class); + protected String _createvmPath; + protected String _listvmdiskPath; + protected String _listvmdisksizePath; + protected String _delvmPath; + protected String _manageSnapshotPath; + protected String _manageVolumePath; + protected String _createPrivateTemplatePath; + protected String _guid; + protected String _rootDir; + protected String _rootFolder = "vmops"; + protected String _parent; + protected String _trashcanDir; + protected String _vmFolder; + protected String _trashcanFolder; + protected String _datadisksFolder; + protected String _datadisksDir; + protected String _sharePath; + protected String _infoPath; + protected int _timeout; + + protected String _iqnPath; + + protected String _checkchildrenPath; + protected String _userPrivateTemplateRootDir; + + protected String _zfsScriptsDir; + + protected DownloadManager _downloadManager; + + protected Map _volumeHourlySnapshotRequests = new HashMap(); + protected Map _volumeDailySnapshotRequests = new HashMap(); + protected String _instance; + + @Override + public Answer executeRequest(final Command cmd) { + if (cmd instanceof DestroyCommand) { + return execute((DestroyCommand)cmd); + } else if (cmd instanceof GetFileStatsCommand) { + return execute((GetFileStatsCommand)cmd); + } else if (cmd instanceof PrimaryStorageDownloadCommand) { + return execute((PrimaryStorageDownloadCommand)cmd); + } else if (cmd instanceof DownloadCommand) { + return execute((DownloadCommand)cmd); + } else if (cmd instanceof GetStorageStatsCommand) { + return execute((GetStorageStatsCommand)cmd); + } else if (cmd instanceof UpgradeDiskCommand) { + return execute((UpgradeDiskCommand) cmd); + } else if (cmd instanceof ShareCommand) { + return execute((ShareCommand)cmd); + } else if (cmd instanceof ManageSnapshotCommand) { + return execute((ManageSnapshotCommand)cmd); + } else if (cmd instanceof BackupSnapshotCommand) { + return execute((BackupSnapshotCommand)cmd); + } else if (cmd instanceof CreatePrivateTemplateCommand) { + return execute((CreatePrivateTemplateCommand)cmd); + } else if (cmd instanceof ModifyStoragePoolCommand ){ + return execute ((ModifyStoragePoolCommand) cmd); + } else { + s_logger.warn("StorageResource: Unsupported command"); + return Answer.createUnsupportedCommandAnswer(cmd); + } + } + + protected Answer execute(ModifyStoragePoolCommand cmd) { + s_logger.warn("Unsupported: network file system mount attempted"); + return Answer.createUnsupportedCommandAnswer(cmd); + } + + protected ShareAnswer execute(final ShareCommand cmd) { + return new ShareAnswer(cmd, new HashMap()); + } + + protected Answer execute(final PrimaryStorageDownloadCommand cmd) { + return Answer.createUnsupportedCommandAnswer(cmd); + } + + protected Answer execute(final DownloadCommand cmd) { + return _downloadManager.handleDownloadCommand(cmd); + } + + public String getSecondaryStorageMountPoint(String uri) { + return null; + } + + protected String getUserPath(final String image) { + return image.substring(0, image.indexOf(File.separator, _parent.length() + 2)).intern(); + } + + + protected Answer execute(final GetFileStatsCommand cmd) { + final String image = cmd.getPaths(); + final Script command = new Script(_listvmdisksizePath, _timeout, s_logger); + command.add("-d", image); + command.add("-a"); + + final SizeParser parser = new SizeParser(); + final String result = command.execute(parser); + if (result != null) { + return new Answer(cmd, false, result); + } + + return new GetFileStatsAnswer(cmd, parser.size); + } + + protected List getVolumes(final String rootdiskFolder, final String datadiskFolder, final String datadiskName) { + final ArrayList vols = new ArrayList(); + + // Get the rootdisk volume + String path = rootdiskFolder + File.separator + "rootdisk"; + long totalSize = getVolumeSize(path); + + VolumeVO vol = new VolumeVO(null, null, -1, -1, -1, -1, new Long(-1), rootdiskFolder, path, totalSize, Volume.VolumeType.ROOT); + vols.add(vol); + + // Get the datadisk volume + if (datadiskFolder != null && datadiskName != null) { + path = datadiskFolder + File.separator + datadiskName; + totalSize = getVolumeSize(path); + + vol = new VolumeVO(null, null, -1, -1, -1, -1, new Long(-1), datadiskFolder, path, totalSize, Volume.VolumeType.DATADISK); + vols.add(vol); + } + + return vols; + } + + protected List getVolumes(final String imagePath) { + final ArrayList vols = new ArrayList(); + + String path = getVolumeName(imagePath, null); + long totalSize = getVolumeSize(path); + + VolumeVO vol = new VolumeVO(null, null, -1, -1, -1, -1, new Long(-1), null, path, totalSize, Volume.VolumeType.ROOT); + + vols.add(vol); + + path = getVolumeName(imagePath, (long)1); + if (path != null) { + totalSize = getVolumeSize(path); + + + vol = new VolumeVO(null, null, -1, -1, -1, -1, new Long(-1), null, path, totalSize, Volume.VolumeType.DATADISK); + vols.add(vol); + } + + return vols; + } + + protected long getVolumeSize(final String volume) { + final Script command = new Script(_listvmdisksizePath, _timeout, s_logger); + + command.add("-d", volume); + command.add("-t"); + + final SizeParser parser = new SizeParser(); + final String result = command.execute(parser); + if (result != null) { + throw new CloudRuntimeException(result); + } + return parser.size; + } + + protected String getVolumeName(final String imagePath, final Long diskNum) { + + final Script command = new Script(_listvmdiskPath, _timeout, s_logger); + command.add("-i", imagePath); + if (diskNum == null) { + command.add("-r"); + } else { + command.add("-d", diskNum.toString()); + } + + final PathParser parser = new PathParser(); + final String result = command.execute(parser); + if (result != null) { + throw new CloudRuntimeException("Can't get volume name due to " + result); + } + + return parser.path; + } + + + protected long convertFilesystemSize(final String size) { + if (size == null) { + return -1; + } + + long multiplier = 1; + if (size.endsWith("T")) { + multiplier = 1024l * 1024l * 1024l * 1024l; + } else if (size.endsWith("G")) { + multiplier = 1024l * 1024l * 1024l; + } else if (size.endsWith("M")) { + multiplier = 1024l * 1024l; + } else if (size.endsWith("K")){ + multiplier = 1024l; + } else { + long num; + try { + num = Long.parseLong(size); + } catch (NumberFormatException e) { + s_logger.debug("Unknow size:" + size); + return 0; + } + return num; + } + + return (long)(Double.parseDouble(size.substring(0, size.length() - 1)) * multiplier); + } + + protected abstract void cleanUpEmptyParents(String imagePath); + protected abstract long getUsedSize() ; + protected abstract long getTotalSize(); + protected abstract String destroy(final String imagePath) ; + protected abstract String createTrashDir(final String imagePath, final StringBuilder path) ; + public abstract boolean existPath(final String path); + public abstract String createPath(final String createPath) ; + protected abstract Answer execute(DestroyCommand cmd) ; + protected abstract UpgradeDiskAnswer execute(final UpgradeDiskCommand cmd); + protected abstract String delete(String imagePath, String extra); + protected abstract Volume.StorageResourceType getStorageResourceType(); + protected abstract void configureFolders(String name, Map params) throws ConfigurationException ; + + + + protected GetStorageStatsAnswer execute(final GetStorageStatsCommand cmd) { + final long size = getUsedSize(); + return size != -1 ? new GetStorageStatsAnswer(cmd, 0, size) : new GetStorageStatsAnswer(cmd, "Unable to get storage stats"); + } + + + + protected ManageSnapshotAnswer execute(final ManageSnapshotCommand cmd) { + final Script command = new Script(_manageSnapshotPath, _timeout, s_logger); + String path = null; + if (cmd.getCommandSwitch().equalsIgnoreCase(ManageSnapshotCommand.DESTROY_SNAPSHOT)) { + path = cmd.getSnapshotPath(); + } else if (cmd.getCommandSwitch().equalsIgnoreCase(ManageSnapshotCommand.CREATE_SNAPSHOT)) { + path = cmd.getVolumePath(); + } + command.add(cmd.getCommandSwitch(), path); + command.add("-n", cmd.getSnapshotName()); + + final String result = command.execute(); + return new ManageSnapshotAnswer(cmd, cmd.getSnapshotId(),cmd.getVolumePath(), (result == null), result); + } + + protected BackupSnapshotAnswer execute(final BackupSnapshotCommand cmd) { + // This is implemented only for XenServerResource + Answer answer = Answer.createUnsupportedCommandAnswer(cmd); + return new BackupSnapshotAnswer(cmd, false, answer.getDetails(), null); + } + + protected CreatePrivateTemplateAnswer execute(final CreatePrivateTemplateCommand cmd) { + final Script command = new Script(_createPrivateTemplatePath, _timeout, s_logger); + + String installDir = _userPrivateTemplateRootDir; + if (installDir.startsWith("/")) { + installDir = installDir.substring(1); + } + + command.add("-p", cmd.getSnapshotPath()); + command.add("-s", cmd.getTemplateName()); + command.add("-d", installDir); + command.add("-u", cmd.getUserFolder()); + String templateName = cmd.getTemplateName().replaceAll(" ", "_"); //hard to pass spaces to shell scripts + if (templateName.length() > 32) { + templateName = templateName.substring(0,31); //truncate + } + command.add("-n", templateName); + + final String result = command.execute(); + CreatePrivateTemplateAnswer answer = new CreatePrivateTemplateAnswer(cmd, (result == null), result, null, 0, null, null); + + if (result == null) { + answer.setPath("/" + installDir + "/" + cmd.getUserFolder() + "/" + templateName); + } + + return answer; + } + + protected String create(final String rootdiskFolder, final int rootDiskSizeGB) { + + final Script command = new Script(_createvmPath, _timeout, s_logger); + command.add("-i", rootdiskFolder); + command.add("-S", Integer.toString(rootDiskSizeGB)); + + return command.execute(); + } + + protected String create(final String templateFolder, final String rootdiskFolder, final String userPath, final String dataPath, String localPath) { + + final Script command = new Script(_createvmPath, _timeout, s_logger); + command.add("-t", templateFolder); + command.add("-i", rootdiskFolder); + command.add("-u", userPath); + if (dataPath != null) { + command.add("-d", dataPath); + } + + return command.execute(); + } + + protected String create(final String templateFolder, final String rootdiskFolder, final String userPath, final String datadiskFolder, final String datadiskName, final int datadiskSize, String localPath) { + + final Script command = new Script(_createvmPath, _timeout, s_logger); + + // for private templates, the script needs the snapshot name being used to create the VM + command.add("-t", templateFolder); + command.add("-i", rootdiskFolder); + command.add("-u", userPath); + if (datadiskSize != 0) { + command.add("-f", datadiskFolder); + command.add("-s", Integer.toString(datadiskSize)); + command.add("-n", datadiskName); + } + + return command.execute(); + } + + @Override + public PingCommand getCurrentStatus(final long id) { + return new PingStorageCommand(Host.Type.Storage, id, new HashMap()); + } + + @Override + public StartupCommand[] initialize() { + final StartupStorageCommand cmd = new StartupStorageCommand(_parent, StoragePoolType.NetworkFilesystem, getTotalSize(), new HashMap()); + cmd.setResourceType(getStorageResourceType()); + cmd.setIqn(getIQN()); + fillNetworkInformation(cmd); + return new StartupCommand [] {cmd}; + } + + protected String getIQN() { + final Script command = new Script(_iqnPath, 500, s_logger); + final OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + final String result = command.execute(parser); + if (result != null) { + throw new CloudRuntimeException("Unable to get iqn: " + result); + } + + return parser.getLine(); + } + + @Override + protected String findScript(String script) { + return Script.findScript(_zfsScriptsDir, script); + } + + @Override + protected abstract String getDefaultScriptsDir(); + + @Override + public boolean configure(final String name, final Map params) throws ConfigurationException { + if (!super.configure(name, params)) { + s_logger.warn("Base class was unable to configure"); + return false; + } + + _zfsScriptsDir = (String)params.get("zfs.scripts.dir"); + if (_zfsScriptsDir == null) { + _zfsScriptsDir = getDefaultScriptsDir(); + } + + String value = (String)params.get("scripts.timeout"); + _timeout = NumbersUtil.parseInt(value, 1440) * 1000; + + _createvmPath = findScript("createvm.sh"); + if (_createvmPath == null) { + throw new ConfigurationException("Unable to find the createvm.sh"); + } + s_logger.info("createvm.sh found in " + _createvmPath); + + _delvmPath = findScript("delvm.sh"); + if (_delvmPath == null) { + throw new ConfigurationException("Unable to find the delvm.sh"); + } + s_logger.info("delvm.sh found in " + _delvmPath); + + _listvmdiskPath = findScript("listvmdisk.sh"); + if (_listvmdiskPath == null) { + throw new ConfigurationException("Unable to find the listvmdisk.sh"); + } + s_logger.info("listvmdisk.sh found in " + _listvmdiskPath); + + _listvmdisksizePath = findScript("listvmdisksize.sh"); + if (_listvmdisksizePath == null) { + throw new ConfigurationException("Unable to find the listvmdisksize.sh"); + } + s_logger.info("listvmdisksize.sh found in " + _listvmdisksizePath); + + _iqnPath = findScript("get_iqn.sh"); + if (_iqnPath == null) { + throw new ConfigurationException("Unable to find get_iqn.sh"); + } + s_logger.info("get_iqn.sh found in " + _iqnPath); + + _manageSnapshotPath = findScript("managesnapshot.sh"); + if (_manageSnapshotPath == null) { + throw new ConfigurationException("Unable to find the managesnapshot.sh"); + } + s_logger.info("managesnapshot.sh found in " + _manageSnapshotPath); + + _manageVolumePath = findScript("managevolume.sh"); + if (_manageVolumePath == null) { + throw new ConfigurationException("Unable to find managevolume.sh"); + } + s_logger.info("managevolume.sh found in " + _manageVolumePath); + + _createPrivateTemplatePath = findScript("create_private_template.sh"); + if (_createPrivateTemplatePath == null) { + throw new ConfigurationException("Unable to find the create_private_template.sh"); + } + s_logger.info("create_private_template.sh found in " + _createPrivateTemplatePath); + + _checkchildrenPath = findScript("checkchildren.sh"); + if (_checkchildrenPath == null) { + throw new ConfigurationException("Unable to find the checkchildren.sh"); + } + + value = (String)params.get("developer"); + boolean isDeveloper = Boolean.parseBoolean(value); + + _instance = (String)params.get("instance"); + /* + String guid = (String)params.get("guid"); + if (!isDeveloper && guid == null) { + throw new ConfigurationException("Unable to find the guid"); + } + _guid = guid;*/ + /* + params.put("template.parent", _parent); + _downloadManager = new DownloadManagerImpl(); + _downloadManager.configure("DownloadManager", params);*/ + + return true; + } + + @Override + public Host.Type getType() { + return Host.Type.Storage; + } + + protected boolean hasChildren(final String path) { + final Script script = new Script(_checkchildrenPath, _timeout, s_logger); + script.add(path); + + return script.execute() != null; // not null means there's children. + } + + + public static class SizeParser extends OutputInterpreter { + long size = 0; + @Override + public String interpret(final BufferedReader reader) throws IOException { + String line = null; + final StringBuilder buff = new StringBuilder(); + while ((line = reader.readLine()) != null) { + buff.append(line); + } + + size = Long.parseLong(buff.toString()); + + return null; + } + } + + public static class PathParser extends OutputInterpreter { + String path; + @Override + public String interpret(final BufferedReader reader) throws IOException { + String line = null; + final StringBuilder buff = new StringBuilder(); + while ((line = reader.readLine()) != null) { + buff.append(line); + } + + path = buff.toString(); + if (path != null && path.length() == 0) { + path = null; + } + + return null; + } + } + + protected class VolumeSnapshotRequest { + private final long _volumeId; + private final String _snapshotPath; + + public VolumeSnapshotRequest(long volumeId, String snapshotPath) { + _volumeId = volumeId; + _snapshotPath = snapshotPath; + } + + public long getVolumeId() { + return _volumeId; + } + + public String getSnapshotPath() { + return _snapshotPath; + } + } +} diff --git a/core/src/com/cloud/storage/StorageStats.java b/core/src/com/cloud/storage/StorageStats.java new file mode 100755 index 00000000000..6b9426bdbed --- /dev/null +++ b/core/src/com/cloud/storage/StorageStats.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +public interface StorageStats { + /** + * @return bytes used by the storage server already. + */ + public long getByteUsed(); + /** + * @return bytes capacity of the storage server + */ + public long getCapacityBytes(); +} diff --git a/core/src/com/cloud/storage/VMTemplateHostVO.java b/core/src/com/cloud/storage/VMTemplateHostVO.java new file mode 100644 index 00000000000..c7d78fd7af0 --- /dev/null +++ b/core/src/com/cloud/storage/VMTemplateHostVO.java @@ -0,0 +1,266 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.utils.db.GenericDaoBase; + +/** + * Join table for storage hosts and templates + * @author chiradeep + * + */ +@Entity +@Table(name="template_host_ref") +public class VMTemplateHostVO implements VMTemplateStorageResourceAssoc { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + Long id; + + @Column(name="host_id") + private long hostId; + + @Column(name="template_id") + private long templateId; + + @Column(name=GenericDaoBase.CREATED_COLUMN) + private Date created = null; + + @Column(name="last_updated") + @Temporal(value=TemporalType.TIMESTAMP) + private Date lastUpdated = null; + + @Column (name="download_pct") + private int downloadPercent; + + @Column (name="size") + private long size; + + @Column (name="download_state") + @Enumerated(EnumType.STRING) + private Status downloadState; + + @Column (name="local_path") + private String localDownloadPath; + + @Column (name="error_str") + private String errorString; + + @Column (name="job_id") + private String jobId; + + @Column (name="pool_id") + private Long poolId; + + @Column (name="install_path") + private String installPath; + + @Column (name="url") + private String downloadUrl; + + @Column(name="is_copy") + private boolean isCopy = false; + + @Column(name="destroyed") + boolean destroyed = false; + + public String getInstallPath() { + return installPath; + } + + public long getHostId() { + return hostId; + } + + public void setHostId(long hostId) { + this.hostId = hostId; + } + + public long getTemplateId() { + return templateId; + } + + public void setTemplateId(long templateId) { + this.templateId = templateId; + } + + public int getDownloadPercent() { + return downloadPercent; + } + + public void setDownloadPercent(int downloadPercent) { + this.downloadPercent = downloadPercent; + } + + public void setDownloadState(Status downloadState) { + this.downloadState = downloadState; + } + + public Long getId() { + return id; + } + + public Date getCreated() { + return created; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(Date date) { + lastUpdated = date; + } + + public void setInstallPath(String installPath) { + this.installPath = installPath; + } + + public Status getDownloadState() { + return downloadState; + } + + public VMTemplateHostVO(long hostId, long templateId) { + super(); + this.hostId = hostId; + this.templateId = templateId; + } + + public VMTemplateHostVO(long hostId, long templateId, Date lastUpdated, + int downloadPercent, Status downloadState, + String localDownloadPath, String errorString, String jobId, + String installPath, String downloadUrl) { + super(); + this.hostId = hostId; + this.templateId = templateId; + this.lastUpdated = lastUpdated; + this.downloadPercent = downloadPercent; + this.downloadState = downloadState; + this.localDownloadPath = localDownloadPath; + this.errorString = errorString; + this.jobId = jobId; + this.installPath = installPath; + this.setDownloadUrl(downloadUrl); + } + + protected VMTemplateHostVO() { + + } + + public void setLocalDownloadPath(String localPath) { + this.localDownloadPath = localPath; + } + + public String getLocalDownloadPath() { + return localDownloadPath; + } + + public void setErrorString(String errorString) { + this.errorString = errorString; + } + + public String getErrorString() { + return errorString; + } + + public void setJobId(String jobId) { + this.jobId = jobId; + } + + public String getJobId() { + return jobId; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof VMTemplateHostVO) { + VMTemplateHostVO other = (VMTemplateHostVO)obj; + if (poolId == null && other.getPoolId() == null) { + return (this.templateId==other.getTemplateId() && this.hostId==other.getHostId()); + } else if (poolId == null && other.getPoolId() != null) { + return false; + } else if (poolId != null && other.getPoolId() == null) { + return false; + } else { + return (this.templateId==other.getTemplateId() && this.hostId==other.getHostId() && poolId.longValue() == other.getPoolId().longValue()); + } + } + return false; + } + + @Override + public int hashCode() { + Long tid = new Long(templateId); + Long hid = new Long(hostId); + return tid.hashCode()+hid.hashCode() + ((poolId != null)?poolId.hashCode():0); + } + + public void setPoolId(Long poolId) { + this.poolId = poolId; + } + + public Long getPoolId() { + return poolId; + } + + public void setSize(long size) { + this.size = size; + } + + public long getSize() { + return size; + } + + + public void setDestroyed(boolean destroyed) { + this.destroyed = destroyed; + } + + public boolean getDestroyed() { + return destroyed; + } + + public void setDownloadUrl(String downloadUrl) { + this.downloadUrl = downloadUrl; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public void setCopy(boolean isCopy) { + this.isCopy = isCopy; + } + + public boolean isCopy() { + return isCopy; + } +} diff --git a/core/src/com/cloud/storage/VMTemplateStoragePoolVO.java b/core/src/com/cloud/storage/VMTemplateStoragePoolVO.java new file mode 100644 index 00000000000..3044e716286 --- /dev/null +++ b/core/src/com/cloud/storage/VMTemplateStoragePoolVO.java @@ -0,0 +1,217 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.utils.db.GenericDaoBase; + +/** + * Join table for storage pools and templates + * @author chiradeep + * + */ +@Entity +@Table(name="template_spool_ref") +public class VMTemplateStoragePoolVO implements VMTemplateStorageResourceAssoc{ + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + Long id; + + @Column(name="pool_id") + private long poolId; + + @Column(name="template_id") long templateId; + + @Column(name=GenericDaoBase.CREATED_COLUMN) Date created = null; + + @Column(name="last_updated") + @Temporal(value=TemporalType.TIMESTAMP) Date lastUpdated = null; + + @Column (name="download_pct") int downloadPercent; + + @Column (name="download_state") + @Enumerated(EnumType.STRING) Status downloadState; + + @Column (name="local_path") String localDownloadPath; + + @Column (name="error_str") String errorString; + + @Column (name="job_id") String jobId; + + @Column (name="install_path") String installPath; + + @Column (name="template_size") long templateSize; + + @Column (name="marked_for_gc") boolean markedForGC; + + public String getInstallPath() { + return installPath; + } + + public long getTemplateSize() { + return templateSize; + } + + public long getPoolId() { + return poolId; + } + + public void setpoolId(long poolId) { + this.poolId = poolId; + } + + public long getTemplateId() { + return templateId; + } + + public void setTemplateId(long templateId) { + this.templateId = templateId; + } + + public int getDownloadPercent() { + return downloadPercent; + } + + public void setDownloadPercent(int downloadPercent) { + this.downloadPercent = downloadPercent; + } + + public void setDownloadState(Status downloadState) { + this.downloadState = downloadState; + } + + public Long getId() { + return id; + } + + public Date getCreated() { + return created; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(Date date) { + lastUpdated = date; + } + + public void setInstallPath(String installPath) { + this.installPath = installPath; + } + + public Status getDownloadState() { + return downloadState; + } + + public VMTemplateStoragePoolVO(long poolId, long templateId) { + super(); + this.poolId = poolId; + this.templateId = templateId; + this.downloadState = Status.NOT_DOWNLOADED; + this.markedForGC = false; + } + + public VMTemplateStoragePoolVO(long poolId, long templateId, Date lastUpdated, + int downloadPercent, Status downloadState, + String localDownloadPath, String errorString, String jobId, + String installPath, long templateSize) { + super(); + this.poolId = poolId; + this.templateId = templateId; + this.lastUpdated = lastUpdated; + this.downloadPercent = downloadPercent; + this.downloadState = downloadState; + this.localDownloadPath = localDownloadPath; + this.errorString = errorString; + this.jobId = jobId; + this.installPath = installPath; + this.templateSize = templateSize; + } + + protected VMTemplateStoragePoolVO() { + + } + + public void setLocalDownloadPath(String localPath) { + this.localDownloadPath = localPath; + } + + public String getLocalDownloadPath() { + return localDownloadPath; + } + + public void setErrorString(String errorString) { + this.errorString = errorString; + } + + public String getErrorString() { + return errorString; + } + + public void setJobId(String jobId) { + this.jobId = jobId; + } + + public String getJobId() { + return jobId; + } + + public void setTemplateSize(long templateSize) { + this.templateSize = templateSize; + } + + public boolean getMarkedForGC() { + return markedForGC; + } + + public void setMarkedForGC(boolean markedForGC) { + this.markedForGC = markedForGC; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof VMTemplateStoragePoolVO) { + VMTemplateStoragePoolVO other = (VMTemplateStoragePoolVO)obj; + return (this.templateId==other.getTemplateId() && this.poolId==other.getPoolId()); + } + return false; + } + + @Override + public int hashCode() { + Long tid = new Long(templateId); + Long hid = new Long(poolId); + return tid.hashCode()+hid.hashCode(); + } + + +} diff --git a/core/src/com/cloud/storage/VMTemplateStorageResourceAssoc.java b/core/src/com/cloud/storage/VMTemplateStorageResourceAssoc.java new file mode 100644 index 00000000000..699f2f8297a --- /dev/null +++ b/core/src/com/cloud/storage/VMTemplateStorageResourceAssoc.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.util.Date; + +/** + * @author chiradeep + * + */ +public interface VMTemplateStorageResourceAssoc { + public static enum Status {UNKNOWN, DOWNLOAD_ERROR, NOT_DOWNLOADED, DOWNLOAD_IN_PROGRESS, DOWNLOADED, ABANDONED} + + public String getInstallPath(); + + public long getTemplateId(); + + public void setTemplateId(long templateId); + + public int getDownloadPercent(); + + public void setDownloadPercent(int downloadPercent); + + public void setDownloadState(Status downloadState); + + public Long getId(); + + public Date getCreated(); + + public Date getLastUpdated(); + + public void setLastUpdated(Date date); + + public void setInstallPath(String installPath); + + public Status getDownloadState(); + + public void setLocalDownloadPath(String localPath); + + public String getLocalDownloadPath(); + + public void setErrorString(String errorString); + + public String getErrorString(); + + public void setJobId(String jobId); + + public String getJobId();; + +} diff --git a/core/src/com/cloud/storage/VMTemplateVO.java b/core/src/com/cloud/storage/VMTemplateVO.java new file mode 100644 index 00000000000..43213545f8f --- /dev/null +++ b/core/src/com/cloud/storage/VMTemplateVO.java @@ -0,0 +1,310 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import java.util.Date; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.TableGenerator; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.async.AsyncInstanceCreateStatus; +import com.cloud.storage.Storage.FileSystem; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.utils.db.GenericDao; +import com.google.gson.annotations.Expose; +import com.cloud.storage.Storage; + +@Entity +@Table(name="vm_template") +public class VMTemplateVO implements VirtualMachineTemplate { + @Id + @TableGenerator(name="vm_template_sq", table="sequence", pkColumnName="name", valueColumnName="value", pkColumnValue="vm_template_seq", allocationSize=1) + @Column(name="id", nullable = false) + private long id; + + @Column(name="format") + private Storage.ImageFormat format; + + @Column(name="unique_name") + private String uniqueName; + + @Column(name="name") + private String name = null; + + @Column(name="public") + private boolean publicTemplate = true; + + @Column(name="featured") + private boolean featured; + + @Column(name="type") + private FileSystem fileSystem = null; + + @Column(name="url") + private String url = null; + + @Column(name="hvm") + private boolean requiresHvm; + + @Column(name="bits") + private int bits; + + @Temporal(value=TemporalType.TIMESTAMP) + @Column(name=GenericDao.CREATED_COLUMN) + private Date created = null; + + @Temporal(value=TemporalType.TIMESTAMP) + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + @Column(name="account_id") + private long accountId; + + @Column(name="checksum") + private String checksum; + + @Column(name="display_text") + private String displayText; + + @Column(name="enable_password") + private boolean enablePassword; + + @Column(name="guest_os_id") + private long guestOSId; + + @Column(name="bootable") + private boolean bootable = true; + + @Column(name="prepopulate") + private boolean prepopulate = false; + + @Column(name="cross_zones") + private boolean crossZones = false; + + public String getUniqueName() { + return uniqueName; + } + + public void setUniqueName(String uniqueName) { + this.uniqueName = uniqueName; + } + + protected VMTemplateVO() { + } + + /** + * Proper constructor for a new vm template. + */ + public VMTemplateVO(long id, String name, ImageFormat format, boolean isPublic, boolean featured, FileSystem fs, String url, boolean requiresHvm, int bits, long accountId, String cksum, String displayText, boolean enablePassword, long guestOSId, boolean bootable) { + this(id, generateUniqueName(id, accountId, name), name, format, isPublic, featured, fs, url, null, requiresHvm, bits, accountId, cksum, displayText, enablePassword, guestOSId, bootable); + } + + public VMTemplateVO(Long id, String uniqueName, String name, ImageFormat format, boolean isPublic, boolean featured, FileSystem fs, String url, Date created, boolean requiresHvm, int bits, long accountId, String cksum, String displayText, boolean enablePassword, long guestOSId, boolean bootable) { + this.id = id; + this.name = name; + this.publicTemplate = isPublic; + this.featured = featured; + this.fileSystem = fs; + this.url = url; + this.requiresHvm = requiresHvm; + this.bits = bits; + this.accountId = accountId; + this.checksum = cksum; + this.uniqueName = uniqueName; + this.displayText = displayText; + this.enablePassword = enablePassword; + this.format = format; + this.created = created; + this.guestOSId = guestOSId; + this.bootable = bootable; + } + + public boolean getEnablePassword() { + return enablePassword; + } + + public Storage.ImageFormat getFormat() { + return format; + } + + public void setEnablePassword(boolean enablePassword) { + this.enablePassword = enablePassword; + } + + public void setFormat(ImageFormat format) { + this.format = format; + } + + private static String generateUniqueName(long id, long userId, String displayName) { + StringBuilder name = new StringBuilder(); + name.append(id); + name.append("-"); + name.append(userId); + name.append("-"); + name.append(UUID.nameUUIDFromBytes((displayName + System.currentTimeMillis()).getBytes()).toString()); + return name.toString(); + } + + @Override + public Long getId() { + return id; + } + + @Override + public FileSystem getFileSystem() { + return fileSystem; + } + + public void setFileSystem(FileSystem fs) { + this.fileSystem = fs; + } + + public boolean requiresHvm() { + return requiresHvm; + } + + public int getBits() { + return bits; + } + + public void setBits(int bits) { + this.bits = bits; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getRemoved() { + return removed; + } + + @Override + public boolean isPublicTemplate() { + return publicTemplate; + } + + public void setPublicTemplate(boolean publicTemplate) { + this.publicTemplate = publicTemplate; + } + + public boolean isFeatured() { + return featured; + } + + public void setFeatured(boolean featured) { + this.featured = featured; + } + + public Date getCreated() { + return created; + } + + public String getUrl() { + return url; + } + + public boolean isRequiresHvm() { + return requiresHvm; + } + + public void setRequiresHvm(boolean value) { + requiresHvm = value; + } + + public long getAccountId() { + return accountId; + } + + public String getChecksum() { + return checksum; + } + + public String getDisplayText() { + return displayText; + } + + public void setDisplayText(String displayText) { + this.displayText = displayText; + } + + public long getGuestOSId() { + return guestOSId; + } + + public void setGuestOSId(long guestOSId) { + this.guestOSId = guestOSId; + } + + public boolean isBootable() { + return bootable; + } + + public void setBootable(boolean bootable) { + this.bootable = bootable; + } + + public void setPrepopulate(boolean prepopulate) { + this.prepopulate = prepopulate; + } + + public boolean isPrepopulate() { + return prepopulate; + } + + public void setCrossZones(boolean crossZones) { + this.crossZones = crossZones; + } + + public boolean isCrossZones() { + return crossZones; + } + + @Override + public boolean equals(Object that) { + if (this == that ) + return true; + if (!(that instanceof VMTemplateVO)){ + return false; + } + VMTemplateVO other = (VMTemplateVO)that; + + return (this.getUniqueName().equals(other.getUniqueName())); + + } + + @Override + public int hashCode() { + return uniqueName.hashCode(); + } + +} diff --git a/core/src/com/cloud/storage/VMTemplateZoneVO.java b/core/src/com/cloud/storage/VMTemplateZoneVO.java new file mode 100644 index 00000000000..6e88e2ffca9 --- /dev/null +++ b/core/src/com/cloud/storage/VMTemplateZoneVO.java @@ -0,0 +1,117 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.GenericDaoBase; + +@Entity +@Table(name="template_zone_ref") +public class VMTemplateZoneVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + Long id; + + @Column(name="zone_id") + private long zoneId; + + @Column(name="template_id") + private long templateId; + + @Column(name=GenericDaoBase.CREATED_COLUMN) + private Date created = null; + + @Column(name="last_updated") + @Temporal(value=TemporalType.TIMESTAMP) + private Date lastUpdated = null; + + @Temporal(value=TemporalType.TIMESTAMP) + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + protected VMTemplateZoneVO() { + + } + + public VMTemplateZoneVO(long zoneId, long templateId, Date lastUpdated) { + this.zoneId = zoneId; + this.templateId = templateId; + this.lastUpdated = lastUpdated; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public long getZoneId() { + return zoneId; + } + + public void setZoneId(long zoneId) { + this.zoneId = zoneId; + } + + public long getTemplateId() { + return templateId; + } + + public void setTemplateId(long templateId) { + this.templateId = templateId; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(Date lastUpdated) { + this.lastUpdated = lastUpdated; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public Date getRemoved() { + return removed; + } + +} diff --git a/core/src/com/cloud/storage/VirtualMachineTemplate.java b/core/src/com/cloud/storage/VirtualMachineTemplate.java new file mode 100755 index 00000000000..7a56a2c9477 --- /dev/null +++ b/core/src/com/cloud/storage/VirtualMachineTemplate.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import com.cloud.async.AsyncInstanceCreateStatus; +import com.cloud.storage.Storage.FileSystem; + +public interface VirtualMachineTemplate { + + public static enum BootloaderType { PyGrub, HVM, External }; + + /** + * @return id. + */ + Long getId(); + + /** + * @return public or private template + */ + boolean isPublicTemplate(); + + /** + * @return name + */ + String getName(); + + /** + * @return the file system for this template. + */ + FileSystem getFileSystem(); + +} diff --git a/core/src/com/cloud/storage/Volume.java b/core/src/com/cloud/storage/Volume.java new file mode 100755 index 00000000000..45d22ee4018 --- /dev/null +++ b/core/src/com/cloud/storage/Volume.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import com.cloud.async.AsyncInstanceCreateStatus; + +public interface Volume { + enum VolumeType {UNKNOWN, ROOT, SWAP, DATADISK}; + + enum MirrorState {NOT_MIRRORED, ACTIVE, DEFUNCT}; + + enum StorageResourceType {STORAGE_POOL, STORAGE_HOST, SECONDARY_STORAGE}; + + /** + * @return the volume name + */ + String getName(); + + /** + * @return owner's account id + */ + long getAccountId(); + + /** + * @return id of the owning account's domain + */ + long getDomainId(); + + /** + * @return total size of the partition + */ + long getSize(); + + void setSize(long size); + + /** + * @return the vm instance id + */ + Long getInstanceId(); + + /** + * @return the folder of the volume + */ + String getFolder(); + + /** + * @return the path created. + */ + String getPath(); + + Long getPodId(); + + long getDataCenterId(); + + VolumeType getVolumeType(); + + StorageResourceType getStorageResourceType(); + + Long getPoolId(); + + public AsyncInstanceCreateStatus getStatus(); + + public void setStatus(AsyncInstanceCreateStatus status); + +} diff --git a/core/src/com/cloud/storage/VolumeStats.java b/core/src/com/cloud/storage/VolumeStats.java new file mode 100644 index 00000000000..fdeb1bb620d --- /dev/null +++ b/core/src/com/cloud/storage/VolumeStats.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage; + +public interface VolumeStats { + /** + * @return bytes used by the volume + */ + public long getBytesUsed(); +} diff --git a/core/src/com/cloud/storage/VolumeVO.java b/core/src/com/cloud/storage/VolumeVO.java new file mode 100755 index 00000000000..c85d628b8da --- /dev/null +++ b/core/src/com/cloud/storage/VolumeVO.java @@ -0,0 +1,493 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.async.AsyncInstanceCreateStatus; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.utils.db.GenericDao; +import com.google.gson.annotations.Expose; + +@Entity +@Table(name="volumes") +public class VolumeVO implements Volume { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + Long id; + + @Expose + @Column(name="name") + String name; + + @Column(name="pool_id") + Long poolId; + + @Column(name="account_id") + long accountId; + + @Column(name="domain_id") + long domainId; + + @Column(name="instance_id") + Long instanceId = null; + + @Expose + @Column(name="device_id") + Long deviceId = null; + + @Expose + @Column(name="size") + long size; + + @Expose + @Column(name="folder") + String folder; + + @Expose + @Column(name="path") + String path; + + @Expose + @Column(name="iscsi_name") + String iscsiName; + + @Column(name="pod_id") + Long podId; + + @Column(name="destroyed") + boolean destroyed = false; + + @Column(name="created") + Date created; + + @Column(name="data_center_id") + long dataCenterId; + + @Expose + @Column(name="host_ip") + String hostip; + + @Column(name="disk_offering_id") + Long diskOfferingId; + + @Expose + @Column(name="mirror_vol") + Long mirrorVolume; + + @Column(name="template_id") + Long templateId; + + @Column(name="first_snapshot_backup_uuid") + String firstSnapshotBackupUuid; + + @Expose + @Column(name="volume_type") + @Enumerated(EnumType.STRING) + VolumeType volumeType = Volume.VolumeType.UNKNOWN; + + @Expose + @Column(name="mirror_state") + @Enumerated(EnumType.STRING) + MirrorState mirrorState = Volume.MirrorState.NOT_MIRRORED; + + @Expose + @Column(name="pool_type") + @Enumerated(EnumType.STRING) + StoragePoolType poolType; + + @Column(name=GenericDao.REMOVED_COLUMN) + Date removed; + + @Expose + @Column(name="resource_type") + @Enumerated(EnumType.STRING) + StorageResourceType storageResourceType; + + @Expose + @Column(name="status", updatable = true, nullable=false) + @Enumerated(value=EnumType.STRING) + private AsyncInstanceCreateStatus status; + + @Column(name="updated") + @Temporal(value=TemporalType.TIMESTAMP) + Date updated; + + @Column(name="recreatable") + boolean recreatable; + + /** + * Constructor for data disk. + * @param type + * @param instanceId + * @param name + * @param dcId + * @param podId + * @param accountId + * @param domainId + * @param size + */ + public VolumeVO(VolumeType type, long instanceId, String name, long dcId, long podId, long accountId, long domainId, long size) { + this.volumeType = type; + this.name = name; + this.dataCenterId = dcId; + this.podId = podId; + this.accountId = accountId; + this.domainId = domainId; + this.instanceId = instanceId; + this.size = size; + this.status = AsyncInstanceCreateStatus.Creating; + this.templateId = null; + this.mirrorState = MirrorState.NOT_MIRRORED; + this.mirrorVolume = null; + this.storageResourceType = StorageResourceType.STORAGE_POOL; + this.poolType = null; + } + + /** + * Constructor for volume based on a template. + * + * @param type + * @param instanceId + * @param templateId + * @param name + * @param dcId + * @param podId + * @param accountId + * @param domainId + */ + public VolumeVO(VolumeType type, long instanceId, long templateId, String name, long dcId, long podId, long accountId, long domainId, boolean recreatable) { + this(type, instanceId, name, dcId, podId, accountId, domainId, 0l); + this.templateId = templateId; + this.recreatable = recreatable; + } + + + public VolumeVO(Long id, String name, long dcId, long podId, long accountId, long domainId, Long instanceId, String folder, String path, long size, Volume.VolumeType vType) { + this.id = id; + this.name = name; + this.accountId = accountId; + this.domainId = domainId; + this.instanceId = instanceId; + this.folder = folder; + this.path = path; + this.size = size; + this.podId = podId; + this.dataCenterId = dcId; + this.volumeType = vType; + this.status = AsyncInstanceCreateStatus.Created; + this.recreatable = false; + } + + public boolean isRecreatable() { + return recreatable; + } + + public String getIscsiName() { + return iscsiName; + } + + public Long getId() { + return id; + } + + @Override + public Long getPodId() { + return podId; + } + + @Override + public long getDataCenterId() { + return dataCenterId; + } + + @Override + public String getName() { + return name; + } + + @Override + public long getAccountId() { + return accountId; + } + + public void setPoolType(StoragePoolType poolType) { + this.poolType = poolType; + } + + public StoragePoolType getPoolType() { + return poolType; + } + + @Override + public long getDomainId() { + return domainId; + } + + @Override + public String getFolder() { + return folder; + } + + @Override + public String getPath() { + return path; + } + + protected VolumeVO() { + } + + @Override + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + @Override + public Long getInstanceId() { + return instanceId; + } + + public Long getDeviceId() { + return deviceId; + } + + public void setDeviceId(Long deviceId) { + this.deviceId = deviceId; + } + + @Override + public VolumeType getVolumeType() { + return volumeType; + } + + public void setId(Long id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setFolder(String folder) { + this.folder = folder; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + public void setDomainId(long domainId) { + this.domainId = domainId; + } + + public void setInstanceId(Long instanceId) { + this.instanceId = instanceId; + } + + public void setPath(String path) { + this.path = path; + } + + public String getHostIp() { + return hostip; + } + + public void setHostIp(String hostip) { + this.hostip = hostip; + } + + public void setIscsiName(String iscsiName) { + this.iscsiName = iscsiName; + } + + public void setPodId(Long podId) { + this.podId = podId; + } + + public void setDataCenterId(long dataCenterId) { + this.dataCenterId = dataCenterId; + } + + public void setVolumeType(VolumeType type) { + volumeType = type; + } + + public boolean getDestroyed() { + return destroyed; + } + + public Date getCreated() { + return created; + } + + public void setDestroyed(boolean destroyed) { + this.destroyed = destroyed; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public MirrorState getMirrorState() { + return mirrorState; + } + + public void setMirrorState(MirrorState mirrorState) { + this.mirrorState = mirrorState; + } + + public Long getDiskOfferingId() { + return diskOfferingId; + } + + public void setDiskOfferingId(Long diskOfferingId) { + this.diskOfferingId = diskOfferingId; + } + + public Long getTemplateId() { + return templateId; + } + + public void setTemplateId(Long templateId) { + this.templateId = templateId; + } + + public String getFirstSnapshotBackupUuid() { + return firstSnapshotBackupUuid; + } + + public void setFirstSnapshotBackupUuid(String firstSnapshotBackupUuid) { + this.firstSnapshotBackupUuid = firstSnapshotBackupUuid; + } + + public Long getMirrorVolume() { + return mirrorVolume; + } + + public void setMirrorVolume(Long mirrorVolume) { + this.mirrorVolume = mirrorVolume; + } + + @Override + public StorageResourceType getStorageResourceType() { + return storageResourceType; + } + + public void setStorageResourceType(StorageResourceType storageResourceType2) { + this.storageResourceType = storageResourceType2; + } + + public Long getPoolId() { + return poolId; + } + + public void setPoolId(Long poolId) { + this.poolId = poolId; + } + + @Override + public AsyncInstanceCreateStatus getStatus() { + return status; + } + + @Override + public void setStatus(AsyncInstanceCreateStatus status) { + this.status = status; + } + + public Date getUpdated() { + return updated; + } + + public void setUpdated(Date updated) { + this.updated = updated; + } + + public Lun getLun() { + return new Lun(hostip, iscsiName); + } + + public class Lun { + private final String ip; + private String iqn; + private String lun; + + protected Lun(String ip, String iscsiName) { + this.ip = ip; + String[] str = iscsiName.split(":lu:"); + if (str != null && str.length == 2) { + iqn = str[0]; + lun = str[1]; + } else { + iqn = null; + lun = null; + } + } + + public Lun(String ip, String iqn, String lun) { + this.ip = ip; + this.iqn = iqn; + this.lun = lun; + } + + public String getIp() { + return ip; + } + + public String getIqn() { + return iqn; + } + + public String getLun() { + return lun; + } + + public boolean isIscsi() { + return lun != null; + } + + protected String getIscsiName() { + return iqn + ":lu:" + lun; + } + } + + @Override + public String toString() { + return new StringBuilder("Vol[").append(id).append("|vm=").append(instanceId).append("|").append(volumeType).append("]").toString(); + } +} diff --git a/core/src/com/cloud/storage/dao/DiskOfferingDao.java b/core/src/com/cloud/storage/dao/DiskOfferingDao.java new file mode 100644 index 00000000000..0cfe20e3aea --- /dev/null +++ b/core/src/com/cloud/storage/dao/DiskOfferingDao.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.DiskOfferingVO; +import com.cloud.utils.db.GenericDao; + +public interface DiskOfferingDao extends GenericDao { + List listByDomainId(long domainId); + List findPrivateDiskOffering(); + +} diff --git a/core/src/com/cloud/storage/dao/DiskOfferingDaoImpl.java b/core/src/com/cloud/storage/dao/DiskOfferingDaoImpl.java new file mode 100644 index 00000000000..76811aa0eec --- /dev/null +++ b/core/src/com/cloud/storage/dao/DiskOfferingDaoImpl.java @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.DiskOfferingVO.Type; +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.SearchCriteria.Op; + +@Local(value={DiskOfferingDao.class}) +public class DiskOfferingDaoImpl extends GenericDaoBase implements DiskOfferingDao { + private static final Logger s_logger = Logger.getLogger(DiskOfferingDaoImpl.class); + + private final SearchBuilder DomainIdSearch; + private final SearchBuilder PrivateDiskOfferingSearch; + private final Attribute _typeAttr; + + protected DiskOfferingDaoImpl() { + DomainIdSearch = createSearchBuilder(); + DomainIdSearch.and("domainId", DomainIdSearch.entity().getDomainId(), SearchCriteria.Op.EQ); + DomainIdSearch.done(); + + PrivateDiskOfferingSearch = createSearchBuilder(); + PrivateDiskOfferingSearch.and("diskSize", PrivateDiskOfferingSearch.entity().getDiskSize(), SearchCriteria.Op.EQ); + PrivateDiskOfferingSearch.done(); + + _typeAttr = _allAttributes.get("type"); + } + + @Override + public List listByDomainId(long domainId) { + SearchCriteria sc = DomainIdSearch.create(); + sc.setParameters("domainId", domainId); + // FIXME: this should not be exact match, but instead should find all available disk offerings from parent domains + return listActiveBy(sc); + } + + @Override + public List findPrivateDiskOffering() { + SearchCriteria sc = PrivateDiskOfferingSearch.create(); + sc.setParameters("diskSize", 0); + return listActiveBy(sc); + } + + @Override + public List searchAll(SearchCriteria sc, final Filter filter, final Boolean lock, final boolean cache) { + sc.addAnd(_typeAttr, Op.EQ, Type.Disk); + return super.searchAll(sc, filter, lock, cache); + } + + @Override + public List searchAll(SearchCriteria sc, final Filter filter) { + sc.addAnd(_typeAttr, Op.EQ, Type.Disk); + return super.searchAll(sc, filter); + } + + @Override + protected List executeList(final String sql, final Object... params) { + StringBuilder builder = new StringBuilder(sql); + int index = builder.indexOf("WHERE"); + if (index == -1) { + builder.append(" WHERE type=?"); + } else { + builder.insert(index + 6, "type=? "); + } + + return super.executeList(sql, Type.Disk, params); + } +} diff --git a/core/src/com/cloud/storage/dao/DiskTemplateDao.java b/core/src/com/cloud/storage/dao/DiskTemplateDao.java new file mode 100755 index 00000000000..523155d589a --- /dev/null +++ b/core/src/com/cloud/storage/dao/DiskTemplateDao.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import com.cloud.storage.DiskTemplateVO; +import com.cloud.utils.db.GenericDao; + +public interface DiskTemplateDao extends GenericDao { + public DiskTemplateVO findByTypeAndSize(String type, long size); + +} diff --git a/core/src/com/cloud/storage/dao/DiskTemplateDaoImpl.java b/core/src/com/cloud/storage/dao/DiskTemplateDaoImpl.java new file mode 100755 index 00000000000..4b2a4b96ab0 --- /dev/null +++ b/core/src/com/cloud/storage/dao/DiskTemplateDaoImpl.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.storage.DiskTemplateVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={DiskTemplateDao.class}) +public class DiskTemplateDaoImpl extends GenericDaoBase implements DiskTemplateDao { + + protected SearchBuilder TypeSizeSearch; + + protected static final String BY_TYPE_AND_SIZE_CLAUSE = "type = ? AND size = ?"; + + protected DiskTemplateDaoImpl() { + super(); + + TypeSizeSearch = createSearchBuilder(); + TypeSizeSearch.and("type", TypeSizeSearch.entity().getType(), SearchCriteria.Op.EQ); + TypeSizeSearch.and("size", TypeSizeSearch.entity().getSize(), SearchCriteria.Op.EQ); + } + + public DiskTemplateVO findByTypeAndSize(String type, long size) { + SearchCriteria sc = TypeSizeSearch.create(); + sc.setParameters("type", type); + sc.setParameters("size", size); + + List vos = listActiveBy(sc); + assert(vos.size() <= 1); // Should only have one. If more than one something is wrong. + return vos.size() == 0 ? null : vos.get(0); + } +} diff --git a/core/src/com/cloud/storage/dao/GuestOSCategoryDao.java b/core/src/com/cloud/storage/dao/GuestOSCategoryDao.java new file mode 100644 index 00000000000..30ae45bdf6b --- /dev/null +++ b/core/src/com/cloud/storage/dao/GuestOSCategoryDao.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import com.cloud.storage.GuestOSCategoryVO; +import com.cloud.utils.db.GenericDao; + +public interface GuestOSCategoryDao extends GenericDao { + +} diff --git a/core/src/com/cloud/storage/dao/GuestOSCategoryDaoImpl.java b/core/src/com/cloud/storage/dao/GuestOSCategoryDaoImpl.java new file mode 100644 index 00000000000..33441db6d0d --- /dev/null +++ b/core/src/com/cloud/storage/dao/GuestOSCategoryDaoImpl.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import javax.ejb.Local; + +import com.cloud.storage.GuestOSCategoryVO; +import com.cloud.utils.db.GenericDaoBase; + +@Local (value={GuestOSCategoryDao.class}) +public class GuestOSCategoryDaoImpl extends GenericDaoBase implements GuestOSCategoryDao { + + protected GuestOSCategoryDaoImpl() { + + } + +} diff --git a/core/src/com/cloud/storage/dao/GuestOSDao.java b/core/src/com/cloud/storage/dao/GuestOSDao.java new file mode 100644 index 00000000000..dd19fb8cef0 --- /dev/null +++ b/core/src/com/cloud/storage/dao/GuestOSDao.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import com.cloud.storage.GuestOSVO; +import com.cloud.utils.db.GenericDao; + +public interface GuestOSDao extends GenericDao { + +} diff --git a/core/src/com/cloud/storage/dao/GuestOSDaoImpl.java b/core/src/com/cloud/storage/dao/GuestOSDaoImpl.java new file mode 100644 index 00000000000..eea950fe430 --- /dev/null +++ b/core/src/com/cloud/storage/dao/GuestOSDaoImpl.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import javax.ejb.Local; + +import com.cloud.storage.GuestOSVO; +import com.cloud.utils.db.GenericDaoBase; + +@Local (value={GuestOSDao.class}) +public class GuestOSDaoImpl extends GenericDaoBase implements GuestOSDao { + + protected GuestOSDaoImpl() { + + } + +} diff --git a/core/src/com/cloud/storage/dao/LaunchPermissionDao.java b/core/src/com/cloud/storage/dao/LaunchPermissionDao.java new file mode 100644 index 00000000000..995191dd893 --- /dev/null +++ b/core/src/com/cloud/storage/dao/LaunchPermissionDao.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.LaunchPermissionVO; +import com.cloud.storage.VMTemplateVO; +import com.cloud.utils.db.GenericDao; + +public interface LaunchPermissionDao extends GenericDao { + /** + * remove the ability to launch vms from the given template for the given account names which are valid in the given domain + * @param templateId id of the template to modify launch permissions + * @param accountIds list of account ids + */ + void removePermissions(long templateId, List accountIds); + + /** + * remove all launch permissions associated with a template + * @param templateId + */ + void removeAllPermissions(long templateId); + + /** + * Find a launch permission by templateId, accountName, and domainId + * @param templateId the id of the template to search for + * @param accountId the id of the account for which permission is being searched + * @return launch permission if found, null otherwise + */ + LaunchPermissionVO findByTemplateAndAccount(long templateId, long accountId); + + /** + * List all launch permissions for the given template + * @param templateId id of the template for which launch permissions will be queried + * @return list of launch permissions + */ + List findByTemplate(long templateId); + + /** + * List all templates for which permission to launch instances has been granted to the given account + * @param accountId + * @return + */ + List listPermittedTemplates(long accountId); +} diff --git a/core/src/com/cloud/storage/dao/LaunchPermissionDaoImpl.java b/core/src/com/cloud/storage/dao/LaunchPermissionDaoImpl.java new file mode 100644 index 00000000000..f3b076619bb --- /dev/null +++ b/core/src/com/cloud/storage/dao/LaunchPermissionDaoImpl.java @@ -0,0 +1,158 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.storage.LaunchPermissionVO; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.Storage.FileSystem; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.utils.DateUtil; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.exception.CloudRuntimeException; + +@Local(value={LaunchPermissionDao.class}) +public class LaunchPermissionDaoImpl extends GenericDaoBase implements LaunchPermissionDao { + private static final Logger s_logger = Logger.getLogger(LaunchPermissionDaoImpl.class); + private static final String REMOVE_LAUNCH_PERMISSION = "DELETE FROM `cloud`.`launch_permission`" + + " WHERE template_id = ? AND account_id = ?"; + + private static final String LIST_PERMITTED_TEMPLATES = "SELECT t.id, t.unique_name, t.name, t.public, t.format, t.type, t.hvm, t.bits, t.url, t.created, t.account_id, t.checksum, t.display_text, t.enable_password, t.guest_os_id, t.featured" + + " FROM `cloud`.`vm_template` t INNER JOIN (SELECT lp.template_id as lptid" + + " FROM `cloud`.`launch_permission` lp" + + " WHERE lp.account_id = ?) joinlp" + + " WHERE t.id = joinlp.lptid" + + " ORDER BY t.created DESC"; + + private final SearchBuilder TemplateAndAccountSearch; + private final SearchBuilder TemplateIdSearch; + + protected LaunchPermissionDaoImpl() { + TemplateAndAccountSearch = createSearchBuilder(); + TemplateAndAccountSearch.and("templateId", TemplateAndAccountSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateAndAccountSearch.and("accountId", TemplateAndAccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + TemplateAndAccountSearch.done(); + + TemplateIdSearch = createSearchBuilder(); + TemplateIdSearch.and("templateId", TemplateIdSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateIdSearch.done(); + } + + @Override + public void removePermissions(long templateId, List accountIds) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + txn.start(); + String sql = REMOVE_LAUNCH_PERMISSION; + pstmt = txn.prepareAutoCloseStatement(sql); + for (Long accountId : accountIds) { + pstmt.setLong(1, templateId); + pstmt.setLong(2, accountId.longValue()); + pstmt.addBatch(); + } + pstmt.executeBatch(); + txn.commit(); + } catch (Exception e) { + txn.rollback(); + s_logger.warn("Error removing launch permissions", e); + throw new CloudRuntimeException("Error removing launch permissions", e); + } + } + + @Override + public void removeAllPermissions(long templateId) { + SearchCriteria sc = TemplateIdSearch.create(); + sc.setParameters("templateId", templateId); + delete(sc); + } + + @Override + public LaunchPermissionVO findByTemplateAndAccount(long templateId, long accountId) { + SearchCriteria sc = TemplateAndAccountSearch.create(); + sc.setParameters("templateId", templateId); + sc.setParameters("accountId", accountId); + return findOneActiveBy(sc); + } + + @Override + public List listPermittedTemplates(long accountId) { + Transaction txn = Transaction.currentTxn(); + List permittedTemplates = new ArrayList(); + PreparedStatement pstmt = null; + try { + String sql = LIST_PERMITTED_TEMPLATES; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, accountId); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + long id = rs.getLong(1); + String uniqueName = rs.getString(2); + String name = rs.getString(3); + boolean isPublic = rs.getBoolean(4); + String value = rs.getString(5); + ImageFormat format = ImageFormat.valueOf(value); + String filesystem = rs.getString(6); + boolean requiresHVM = rs.getBoolean(7); + int bits = rs.getInt(8); + String url = rs.getString(9); + String createdTS = rs.getString(10); + long templateAccountId = rs.getLong(11); + String checksum = rs.getString(12); + String displayText = rs.getString(13); + boolean enablePassword = rs.getBoolean(14); + long guestOSId = rs.getLong(15); + boolean featured = rs.getBoolean(16); + Date createdDate = null; + + if (createdTS != null) { + createdDate = DateUtil.parseDateString(s_gmtTimeZone, createdTS); + } + + if (isPublic) { + continue; // if it's public already, skip adding it to permitted templates as this for private templates only + } + VMTemplateVO template = new VMTemplateVO(id, uniqueName, name, format, isPublic, featured, FileSystem.valueOf(filesystem), url, createdDate, requiresHVM, bits, templateAccountId, checksum, displayText, enablePassword, guestOSId, true); + permittedTemplates.add(template); + } + } catch (Exception e) { + s_logger.warn("Error listing permitted templates", e); + } + return permittedTemplates; + } + + @Override + public List findByTemplate(long templateId) { + SearchCriteria sc = TemplateIdSearch.create(); + sc.setParameters("templateId", templateId); + return listActiveBy(sc); + } +} diff --git a/core/src/com/cloud/storage/dao/SnapshotDao.java b/core/src/com/cloud/storage/dao/SnapshotDao.java new file mode 100644 index 00000000000..b424a754ae8 --- /dev/null +++ b/core/src/com/cloud/storage/dao/SnapshotDao.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.SnapshotVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDao; + +public interface SnapshotDao extends GenericDao { + List listByVolumeId(long volumeId); + List listByVolumeId(Filter filter, long volumeId); + SnapshotVO findNextSnapshot(long parentSnapId); + long getLastSnapshot(long volumeId, long snapId); + +} diff --git a/core/src/com/cloud/storage/dao/SnapshotDaoImpl.java b/core/src/com/cloud/storage/dao/SnapshotDaoImpl.java new file mode 100644 index 00000000000..4ca3e303b7d --- /dev/null +++ b/core/src/com/cloud/storage/dao/SnapshotDaoImpl.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.storage.SnapshotVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local (value={SnapshotDao.class}) +public class SnapshotDaoImpl extends GenericDaoBase implements SnapshotDao { + + private final SearchBuilder VolumeIdSearch; + private final SearchBuilder ParentIdSearch; + private final GenericSearchBuilder lastSnapSearch; + + @Override + public SnapshotVO findNextSnapshot(long snapshotId) { + SearchCriteria sc = ParentIdSearch.create(); + sc.setParameters("prevSnapshotId", snapshotId); + return findOneBy(sc); + } + + @Override + public List listByVolumeId(long volumeId) { + return listByVolumeId(null, volumeId); + } + + @Override + public List listByVolumeId(Filter filter, long volumeId ) { + SearchCriteria sc = VolumeIdSearch.create(); + sc.setParameters("volumeId", volumeId); + return listActiveBy(sc, filter); + } + + protected SnapshotDaoImpl() { + VolumeIdSearch = createSearchBuilder(); + VolumeIdSearch.and("volumeId", VolumeIdSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdSearch.done(); + + ParentIdSearch = createSearchBuilder(); + ParentIdSearch.and("prevSnapshotId", ParentIdSearch.entity().getPrevSnapshotId(), SearchCriteria.Op.EQ); + ParentIdSearch.done(); + + lastSnapSearch = createSearchBuilder(Long.class); + lastSnapSearch.select(null, SearchCriteria.Func.MAX, lastSnapSearch.entity().getId()); + lastSnapSearch.and("volumeId", lastSnapSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + lastSnapSearch.and("snapId", lastSnapSearch.entity().getId(), SearchCriteria.Op.NEQ); + lastSnapSearch.done(); + } + + @Override + public long getLastSnapshot(long volumeId, long snapId) { + SearchCriteria sc = lastSnapSearch.create(); + sc.setParameters("volumeId", volumeId); + sc.setParameters("snapId", snapId); + List prevSnapshots = searchAll(sc, null); + if(prevSnapshots != null && prevSnapshots.size() > 0 && prevSnapshots.get(0) != null) { + return prevSnapshots.get(0); + } + return 0; + } + +} diff --git a/core/src/com/cloud/storage/dao/SnapshotPolicyDao.java b/core/src/com/cloud/storage/dao/SnapshotPolicyDao.java new file mode 100644 index 00000000000..c780e4d86cb --- /dev/null +++ b/core/src/com/cloud/storage/dao/SnapshotPolicyDao.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.SnapshotPolicyVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDao; + +/* + * Data Access Object for snapshot_policy table + */ +public interface SnapshotPolicyDao extends GenericDao { + List listByVolumeId(long volumeId); + List listByVolumeId(long volumeId, Filter filter); + SnapshotPolicyVO findOneByVolumeInterval(long volumeId, short interval); + List listActivePolicies(); +} diff --git a/core/src/com/cloud/storage/dao/SnapshotPolicyDaoImpl.java b/core/src/com/cloud/storage/dao/SnapshotPolicyDaoImpl.java new file mode 100644 index 00000000000..4a4091d52f3 --- /dev/null +++ b/core/src/com/cloud/storage/dao/SnapshotPolicyDaoImpl.java @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.storage.SnapshotPolicyVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local (value={SnapshotPolicyDao.class}) +public class SnapshotPolicyDaoImpl extends GenericDaoBase implements SnapshotPolicyDao { + private final SearchBuilder VolumeIdSearch; + private final SearchBuilder VolumeIdIntervalSearch; + private final SearchBuilder ActivePolicySearch; + + @Override + public SnapshotPolicyVO findOneByVolumeInterval(long volumeId, short interval) { + SearchCriteria sc = VolumeIdIntervalSearch.create(); + sc.setParameters("volumeId", volumeId); + sc.setParameters("interval", interval); + return findOneBy(sc); + } + + @Override + public List listByVolumeId(long volumeId) { + return listByVolumeId(volumeId, null); + } + + @Override + public List listByVolumeId(long volumeId, Filter filter) { + SearchCriteria sc = VolumeIdSearch.create(); + sc.setParameters("volumeId", volumeId); + sc.setParameters("active", true); + return listActiveBy(sc, filter); + } + + protected SnapshotPolicyDaoImpl() { + VolumeIdSearch = createSearchBuilder(); + VolumeIdSearch.and("volumeId", VolumeIdSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdSearch.and("active", VolumeIdSearch.entity().isActive(), SearchCriteria.Op.EQ); + VolumeIdSearch.done(); + + VolumeIdIntervalSearch = createSearchBuilder(); + VolumeIdIntervalSearch.and("volumeId", VolumeIdIntervalSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdIntervalSearch.and("interval", VolumeIdIntervalSearch.entity().getInterval(), SearchCriteria.Op.EQ); + VolumeIdIntervalSearch.done(); + + ActivePolicySearch = createSearchBuilder(); + ActivePolicySearch.and("active", ActivePolicySearch.entity().isActive(), SearchCriteria.Op.EQ); + ActivePolicySearch.done(); + } + + @Override + public List listActivePolicies() { + SearchCriteria sc = ActivePolicySearch.create(); + sc.setParameters("active", true); + return listBy(sc); + } +} \ No newline at end of file diff --git a/core/src/com/cloud/storage/dao/SnapshotPolicyRefDao.java b/core/src/com/cloud/storage/dao/SnapshotPolicyRefDao.java new file mode 100644 index 00000000000..c2ed3056df8 --- /dev/null +++ b/core/src/com/cloud/storage/dao/SnapshotPolicyRefDao.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.SnapshotPolicyRefVO; +import com.cloud.utils.db.GenericDao; + +public interface SnapshotPolicyRefDao extends GenericDao { + + SnapshotPolicyRefVO findBySnapPolicy(long snapshotId, long policyId); + + int removeSnapPolicy(long snapshotId, long policyId); + + List listByPolicyId(long policyId, long volumeId); + + List listBySnapshotId(long snapshotId); +} diff --git a/core/src/com/cloud/storage/dao/SnapshotPolicyRefDaoImpl.java b/core/src/com/cloud/storage/dao/SnapshotPolicyRefDaoImpl.java new file mode 100644 index 00000000000..f1d50aafde5 --- /dev/null +++ b/core/src/com/cloud/storage/dao/SnapshotPolicyRefDaoImpl.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.storage.SnapshotPolicyRefVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local (value={SnapshotPolicyRefDao.class}) +public class SnapshotPolicyRefDaoImpl extends GenericDaoBase implements SnapshotPolicyRefDao { + protected final SearchBuilder snapPolicy; + protected final SearchBuilder snapSearch; + protected final SearchBuilder policySearch; + + protected SnapshotPolicyRefDaoImpl() { + snapPolicy = createSearchBuilder(); + snapPolicy.and("snapshotId", snapPolicy.entity().getSnapshotId(), SearchCriteria.Op.EQ); + snapPolicy.and("policyId", snapPolicy.entity().getPolicyId(), SearchCriteria.Op.EQ); + snapPolicy.done(); + + snapSearch = createSearchBuilder(); + snapSearch.and("snapshotId", snapSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ); + snapSearch.done(); + + policySearch = createSearchBuilder(); + policySearch.and("policyId", policySearch.entity().getPolicyId(), SearchCriteria.Op.EQ); + policySearch.and("volumeId", policySearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + policySearch.done(); + } + + @Override + public SnapshotPolicyRefVO findBySnapPolicy(long snapshotId, long policyId) { + SearchCriteria sc = snapPolicy.create(); + sc.setParameters("snapshotId", snapshotId); + sc.setParameters("policyId", policyId); + return findOneBy(sc); + } + + @Override + public int removeSnapPolicy(long snapshotId, long policyId) { + SearchCriteria sc = snapPolicy.create(); + sc.setParameters("snapshotId", snapshotId); + sc.setParameters("policyId", policyId); + return delete(sc); + } + + @Override + public List listBySnapshotId(long snapshotId) { + SearchCriteria sc = snapSearch.create(); + sc.setParameters("snapshotId", snapshotId); + return listBy(sc); + } + + @Override + public List listByPolicyId(long policyId, long volumeId) { + SearchCriteria sc = policySearch.create(); + sc.setParameters("policyId", policyId); + sc.setParameters("volumeId", volumeId); + return listBy(sc); + } +} \ No newline at end of file diff --git a/core/src/com/cloud/storage/dao/SnapshotScheduleDao.java b/core/src/com/cloud/storage/dao/SnapshotScheduleDao.java new file mode 100644 index 00000000000..30af98d93a4 --- /dev/null +++ b/core/src/com/cloud/storage/dao/SnapshotScheduleDao.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + + +import java.util.Date; +import java.util.List; + +import com.cloud.storage.SnapshotScheduleVO; +import com.cloud.utils.db.GenericDao; + +/* + * Data Access Object for snapshot_schedule table + */ +public interface SnapshotScheduleDao extends GenericDao { + + List getCoincidingSnapshotSchedules(long volumeId, Date date); + + List getSchedulesToExecute(Date currentTimestamp); + + SnapshotScheduleVO getCurrentSchedule(Long volumeId, Long policyId, boolean executing); + +} diff --git a/core/src/com/cloud/storage/dao/SnapshotScheduleDaoImpl.java b/core/src/com/cloud/storage/dao/SnapshotScheduleDaoImpl.java new file mode 100644 index 00000000000..dc31230cd5b --- /dev/null +++ b/core/src/com/cloud/storage/dao/SnapshotScheduleDaoImpl.java @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +/** + * + */ +package com.cloud.storage.dao; + +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.storage.SnapshotScheduleVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + + +@Local (value={SnapshotScheduleDao.class}) +public class SnapshotScheduleDaoImpl extends GenericDaoBase implements SnapshotScheduleDao { + protected final SearchBuilder executableSchedulesSearch; + protected final SearchBuilder coincidingSchedulesSearch; + // DB constraint: For a given volume and policyId, there will only be one entry in this table. + + + protected SnapshotScheduleDaoImpl() { + + executableSchedulesSearch = createSearchBuilder(); + executableSchedulesSearch.and("scheduledTimestamp", executableSchedulesSearch.entity().getScheduledTimestamp(), SearchCriteria.Op.LT); + executableSchedulesSearch.and("asyncJobId", executableSchedulesSearch.entity().getAsyncJobId(), SearchCriteria.Op.NULL); + executableSchedulesSearch.done(); + + coincidingSchedulesSearch = createSearchBuilder(); + coincidingSchedulesSearch.and("volumeId", coincidingSchedulesSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + coincidingSchedulesSearch.and("scheduledTimestamp", coincidingSchedulesSearch.entity().getScheduledTimestamp(), SearchCriteria.Op.LT); + coincidingSchedulesSearch.and("asyncJobId", coincidingSchedulesSearch.entity().getAsyncJobId(), SearchCriteria.Op.NULL); + coincidingSchedulesSearch.done(); + + } + + /** + * {@inheritDoc} + */ + @Override + public List getCoincidingSnapshotSchedules(long volumeId, Date date) { + SearchCriteria sc = coincidingSchedulesSearch.create(); + sc.setParameters("volumeId", volumeId); + sc.setParameters("scheduledTimestamp", date); + // Don't return manual snapshots. They will be executed through another code path. + sc.addAnd("policyId", SearchCriteria.Op.NEQ, 1L); + return listActiveBy(sc); + } + + /** + * {@inheritDoc} + */ + @Override + public List getSchedulesToExecute(Date currentTimestamp) { + SearchCriteria sc = executableSchedulesSearch.create(); + sc.setParameters("scheduledTimestamp", currentTimestamp); + // Don't return manual snapshots. They will be executed through another code path. + sc.addAnd("policyId", SearchCriteria.Op.NEQ, 1L); + return listActiveBy(sc); + } + + /** + * {@inheritDoc} + */ + @Override + public SnapshotScheduleVO getCurrentSchedule(Long volumeId, Long policyId, boolean executing) { + assert volumeId != null; + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("volumeId", SearchCriteria.Op.EQ, volumeId); + if (policyId != null) { + sc.addAnd("policyId", SearchCriteria.Op.EQ, policyId); + } + SearchCriteria.Op op = executing ? SearchCriteria.Op.NNULL : SearchCriteria.Op.NULL; + sc.addAnd("asyncJobId", op); + List snapshotSchedules = listActiveBy(sc); + // This will return only one schedule because of a DB uniqueness constraint. + assert (snapshotSchedules.size() <= 1); + if (snapshotSchedules.isEmpty()) { + return null; + } + else { + return snapshotSchedules.get(0); + } + } + +} \ No newline at end of file diff --git a/core/src/com/cloud/storage/dao/StoragePoolDao.java b/core/src/com/cloud/storage/dao/StoragePoolDao.java new file mode 100644 index 00000000000..104f282d1f2 --- /dev/null +++ b/core/src/com/cloud/storage/dao/StoragePoolDao.java @@ -0,0 +1,105 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.cloud.host.Status; +import com.cloud.storage.StoragePoolVO; +import com.cloud.utils.db.GenericDao; +/** + * Data Access Object for storage_pool table + */ +public interface StoragePoolDao extends GenericDao { + + /** + * @param datacenterId -- the id of the datacenter (availability zone) + * @return the list of storage pools in the datacenter + */ + List listByDataCenterId(long datacenterId); + + /** + * @param datacenterId -- the id of the datacenter (availability zone) + * @param podId the id of the pod + * @return the list of storage pools in the datacenter + */ + List listBy(long datacenterId, long podId, Long clusterId); + + /** + * Set capacity of storage pool in bytes + * @param id pool id. + * @param capacity capacity in bytes + */ + void updateCapacity(long id, long capacity); + + /** + * Set available bytes of storage pool in bytes + * @param id pool id. + * @param available available capacity in bytes + */ + void updateAvailable(long id, long available); + + + StoragePoolVO persist(StoragePoolVO pool, Map details); + + /** + * Find pool by name. + * + * @param name name of pool. + * @return the single StoragePoolVO + */ + List findPoolByName(String name); + + /** + * Find pools by the pod that matches the details. + * + * @param podId pod id to find the pools in. + * @param details details to match. All must match for the pool to be returned. + * @return List of StoragePoolVO + */ + List findPoolsByDetails(long dcId, long podId, Long clusterId, Map details); + + List findPoolsByTags(long dcId, long podId, Long clusterId, String[] tags, Boolean shared); + + /** + * Find pool by UUID. + * + * @param uuid uuid of pool. + * @return the single StoragePoolVO + */ + StoragePoolVO findPoolByUUID(String uuid); + + List listByStorageHost(String hostFqdnOrIp); + + StoragePoolVO findPoolByHostPath(long dcId, Long podId, String host, String path); + + List listPoolByHostPath(String host, String path); + + void deleteStoragePoolRecords(ArrayList ids); + + void updateDetails(long poolId, Map details); + + Map getDetails(long poolId); + + List searchForStoragePoolDetails(long poolId, String value); + + long countBy(long podId, Status... statuses); + +} diff --git a/core/src/com/cloud/storage/dao/StoragePoolDaoImpl.java b/core/src/com/cloud/storage/dao/StoragePoolDaoImpl.java new file mode 100644 index 00000000000..700dfccccab --- /dev/null +++ b/core/src/com/cloud/storage/dao/StoragePoolDaoImpl.java @@ -0,0 +1,377 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.storage.StoragePoolDetailVO; +import com.cloud.storage.StoragePoolVO; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.SearchCriteria.Func; +import com.cloud.utils.db.SearchCriteria.Op; +import com.cloud.utils.exception.CloudRuntimeException; + +@Local(value={StoragePoolDao.class}) @DB(txn=false) +public class StoragePoolDaoImpl extends GenericDaoBase implements StoragePoolDao { + private static final Logger s_logger = Logger.getLogger(StoragePoolDaoImpl.class); + protected final SearchBuilder NameSearch; + protected final SearchBuilder UUIDSearch; + protected final SearchBuilder DatacenterSearch; + protected final SearchBuilder DcPodSearch; + protected final SearchBuilder HostSearch; + protected final SearchBuilder HostPathDcPodSearch; + protected final SearchBuilder HostPathDcSearch; + protected final SearchBuilder DcPodAnyClusterSearch; + protected final SearchBuilder DeleteLvmSearch; + protected final GenericSearchBuilder MaintenanceCountSearch; + + protected final StoragePoolDetailsDao _detailsDao; + + private final String DetailsSqlPrefix = "SELECT storage_pool.* from storage_pool LEFT JOIN storage_pool_details ON storage_pool.id = storage_pool_details.pool_id WHERE storage_pool.data_center_id = ? and (storage_pool.pod_id = ? or storage_pool.pod_id is null) and ("; + private final String DetailsSqlSuffix = ") GROUP BY storage_pool_details.pool_id HAVING COUNT(storage_pool_details.name) >= ?"; + private final String FindPoolTagDetails = "SELECT storage_pool_details.name FROM storage_pool_details WHERE pool_id = ? and value = ?"; + + protected StoragePoolDaoImpl() { + NameSearch = createSearchBuilder(); + NameSearch.and("name", NameSearch.entity().getName(), SearchCriteria.Op.EQ); + NameSearch.done(); + + UUIDSearch = createSearchBuilder(); + UUIDSearch.and("uuid", UUIDSearch.entity().getUuid(), SearchCriteria.Op.EQ); + UUIDSearch.done(); + + DatacenterSearch = createSearchBuilder(); + DatacenterSearch.and("datacenterId", DatacenterSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DatacenterSearch.done(); + + DcPodSearch = createSearchBuilder(); + DcPodSearch.and("datacenterId", DcPodSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DcPodSearch.and().op("nullpod", DcPodSearch.entity().getPodId(), SearchCriteria.Op.NULL); + DcPodSearch.or("podId", DcPodSearch.entity().getPodId(), SearchCriteria.Op.EQ); + DcPodSearch.cp(); + DcPodSearch.and().op("nullcluster", DcPodSearch.entity().getClusterId(), SearchCriteria.Op.NULL); + DcPodSearch.or("cluster", DcPodSearch.entity().getClusterId(), SearchCriteria.Op.EQ); + DcPodSearch.cp(); + DcPodSearch.done(); + + DcPodAnyClusterSearch = createSearchBuilder(); + DcPodAnyClusterSearch.and("datacenterId", DcPodAnyClusterSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DcPodAnyClusterSearch.and().op("nullpod", DcPodAnyClusterSearch.entity().getPodId(), SearchCriteria.Op.NULL); + DcPodAnyClusterSearch.or("podId", DcPodAnyClusterSearch.entity().getPodId(), SearchCriteria.Op.EQ); + DcPodAnyClusterSearch.cp(); + DcPodAnyClusterSearch.done(); + + DeleteLvmSearch = createSearchBuilder(); + DeleteLvmSearch.and("ids", DeleteLvmSearch.entity().getId(), SearchCriteria.Op.IN); + DeleteLvmSearch.and().op("LVM", DeleteLvmSearch.entity().getPoolType(), SearchCriteria.Op.EQ); + DeleteLvmSearch.or("Filesystem", DeleteLvmSearch.entity().getPoolType(), SearchCriteria.Op.EQ); + DeleteLvmSearch.cp(); + DeleteLvmSearch.done(); + + HostSearch = createSearchBuilder(); + HostSearch.and("host", HostSearch.entity().getHostAddress(), SearchCriteria.Op.EQ); + HostSearch.done(); + + HostPathDcPodSearch = createSearchBuilder(); + HostPathDcPodSearch.and("hostAddress", HostPathDcPodSearch.entity().getHostAddress(), SearchCriteria.Op.EQ); + HostPathDcPodSearch.and("path", HostPathDcPodSearch.entity().getPath(), SearchCriteria.Op.EQ); + HostPathDcPodSearch.and("datacenterId", HostPathDcPodSearch.entity().getDataCenterId(), Op.EQ); + HostPathDcPodSearch.and("podId", HostPathDcPodSearch.entity().getPodId(), Op.EQ); + HostPathDcPodSearch.done(); + + HostPathDcSearch = createSearchBuilder(); + HostPathDcSearch.and("hostAddress", HostPathDcSearch.entity().getHostAddress(), SearchCriteria.Op.EQ); + HostPathDcSearch.and("path", HostPathDcSearch.entity().getPath(), SearchCriteria.Op.EQ); + HostPathDcSearch.done(); + + MaintenanceCountSearch = createSearchBuilder(Long.class); + MaintenanceCountSearch.and("pool", MaintenanceCountSearch.entity().getId(), SearchCriteria.Op.EQ); + MaintenanceCountSearch.select(null, Func.COUNT, null); + MaintenanceCountSearch.and("status", MaintenanceCountSearch.entity().getStatus(), SearchCriteria.Op.IN); + MaintenanceCountSearch.done(); + + _detailsDao = ComponentLocator.inject(StoragePoolDetailsDaoImpl.class); + } + + @Override + public List findPoolByName(String name) { + SearchCriteria sc = NameSearch.create(); + sc.setParameters("name", name); + return listBy(sc); + } + + + @Override + public StoragePoolVO findPoolByUUID(String uuid) { + SearchCriteria sc = UUIDSearch.create(); + sc.setParameters("uuid", uuid); + return findOneBy(sc); + } + + + @Override + public List listByDataCenterId(long datacenterId) { + SearchCriteria sc = DatacenterSearch.create(); + sc.setParameters("datacenterId", datacenterId); + return listBy(sc); + } + + + @Override + public void updateAvailable(long id, long available) { + StoragePoolVO pool = createForUpdate(id); + pool.setAvailableBytes(available); + update(id, pool); + } + + + @Override + public void updateCapacity(long id, long capacity) { + StoragePoolVO pool = createForUpdate(id); + pool.setCapacityBytes(capacity); + update(id, pool); + + } + + @Override + public List listByStorageHost(String hostFqdnOrIp) { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("host", hostFqdnOrIp); + return listBy(sc); + } + + @Override + public StoragePoolVO findPoolByHostPath(long datacenterId, Long podId, String host, String path) { + SearchCriteria sc = HostPathDcPodSearch.create(); + sc.setParameters("hostAddress", host); + sc.setParameters("path", path); + sc.setParameters("datacenterId", datacenterId); + sc.setParameters("podId", podId); + + return findOneBy(sc); + } + + @Override + public List listBy(long datacenterId, long podId, Long clusterId) { + if (clusterId != null) { + SearchCriteria sc = DcPodSearch.create(); + sc.setParameters("datacenterId", datacenterId); + sc.setParameters("podId", podId); + + sc.setParameters("cluster", clusterId); + return listActiveBy(sc); + } else { + SearchCriteria sc = DcPodAnyClusterSearch.create(); + sc.setParameters("datacenterId", datacenterId); + sc.setParameters("podId", podId); + return listActiveBy(sc); + } + } + + @Override + public List listPoolByHostPath(String host, String path) { + SearchCriteria sc = HostPathDcSearch.create(); + sc.setParameters("hostAddress", host); + sc.setParameters("path", path); + + return listBy(sc); + } + + public StoragePoolVO listById(Integer id) + { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("id", id); + + return findOneBy(sc); + } + + @Override @DB + public StoragePoolVO persist(StoragePoolVO pool, Map details) { + Transaction txn = Transaction.currentTxn(); + txn.start(); + pool = super.persist(pool); + if (details != null) { + for (Map.Entry detail : details.entrySet()) { + StoragePoolDetailVO vo = new StoragePoolDetailVO(pool.getId(), detail.getKey(), detail.getValue()); + _detailsDao.persist(vo); + } + } + txn.commit(); + return pool; + } + + @DB + @Override + public void deleteStoragePoolRecords(ArrayList ids) + { + SearchCriteria sc = DeleteLvmSearch.create(); + sc.setParameters("ids", ids.toArray()); + sc.setParameters("LVM", StoragePoolType.LVM); + sc.setParameters("Filesystem", StoragePoolType.Filesystem); + remove(sc); + } + + @DB + @Override + public List findPoolsByDetails(long dcId, long podId, Long clusterId, Map details) { + StringBuilder sql = new StringBuilder(DetailsSqlPrefix); + if (clusterId != null) { + sql.append("storage_pool.cluster_id = ? OR storage_pool.cluster_id IS NULL) AND ("); + } + Set> entries = details.entrySet(); + for (Map.Entry detail : details.entrySet()) { + sql.append("((storage_pool_details.name='").append(detail.getKey()).append("') AND (storage_pool_details.value='").append(detail.getValue()).append("')) OR "); + } + sql.delete(sql.length() - 4, sql.length()); + sql.append(DetailsSqlSuffix); + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = s_initStmt; + try { + pstmt = txn.prepareAutoCloseStatement(sql.toString()); + int i = 1; + pstmt.setLong(i++, dcId); + pstmt.setLong(i++, podId); + if (clusterId != null) { + pstmt.setLong(i++, clusterId); + } + pstmt.setInt(i++, details.size()); + ResultSet rs = pstmt.executeQuery(); + List pools = new ArrayList(); + while (rs.next()) { + pools.add(toEntityBean(rs, false)); + } + return pools; + } catch (SQLException e) { + throw new CloudRuntimeException("Unable to execute " + pstmt.toString(), e); + } + } + + protected Map tagsToDetails(String[] tags) { + Map details = new HashMap(tags.length); + for (String tag: tags) { + details.put(tag, "true"); + } + return details; + } + + @Override + public List findPoolsByTags(long dcId, long podId, Long clusterId, String[] tags, Boolean shared) { + List storagePools = null; + if (tags == null || tags.length == 0) { + storagePools = listBy(dcId, podId, clusterId); + } else { + Map details = tagsToDetails(tags); + storagePools = findPoolsByDetails(dcId, podId, clusterId, details); + } + + if (shared == null) { + return storagePools; + } else { + List filteredStoragePools = new ArrayList(storagePools); + for (StoragePoolVO pool : storagePools) { + if (shared != pool.isShared()) { + filteredStoragePools.remove(pool); + } + } + + return filteredStoragePools; + } + } + + @Override + @DB + public List searchForStoragePoolDetails(long poolId, String value){ + + StringBuilder sql = new StringBuilder(FindPoolTagDetails); + + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(sql.toString()); + pstmt.setLong(1, poolId); + pstmt.setString(2, value); + + ResultSet rs = pstmt.executeQuery(); + List tags = new ArrayList(); + + while (rs.next()) { + tags.add(rs.getString("name")); + } + + return tags; + } catch (SQLException e) { + throw new CloudRuntimeException("Unable to execute " + pstmt.toString(), e); + } + + } + + @Override + public void updateDetails(long poolId, Map details) { + if (details != null) { + _detailsDao.update(poolId, details); + } + } + + @Override + public Map getDetails(long poolId) { + return _detailsDao.getDetails(poolId); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + _detailsDao.configure("DetailsDao", params); + return true; + } + + @Override + public long countBy(long primaryStorageId, Status... statuses) { + SearchCriteria sc = MaintenanceCountSearch.create(); + + sc.setParameters("status", (Object[])statuses); + sc.setParameters("pool", primaryStorageId); + + List rs = searchAll(sc, null); + if (rs.size() == 0) { + return 0; + } + + return rs.get(0); + } +} diff --git a/core/src/com/cloud/storage/dao/StoragePoolDetailsDao.java b/core/src/com/cloud/storage/dao/StoragePoolDetailsDao.java new file mode 100644 index 00000000000..d77cedd32ef --- /dev/null +++ b/core/src/com/cloud/storage/dao/StoragePoolDetailsDao.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import java.util.Map; + +import com.cloud.storage.StoragePoolDetailVO; +import com.cloud.utils.db.GenericDao; + +public interface StoragePoolDetailsDao extends GenericDao { + + void update(long poolId, Map details); + Map getDetails(long poolId); +} diff --git a/core/src/com/cloud/storage/dao/StoragePoolDetailsDaoImpl.java b/core/src/com/cloud/storage/dao/StoragePoolDetailsDaoImpl.java new file mode 100644 index 00000000000..a063a212e0e --- /dev/null +++ b/core/src/com/cloud/storage/dao/StoragePoolDetailsDaoImpl.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ejb.Local; + +import com.cloud.storage.StoragePoolDetailVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value=StoragePoolDetailsDao.class) +public class StoragePoolDetailsDaoImpl extends GenericDaoBase implements StoragePoolDetailsDao { + + protected final SearchBuilder PoolSearch; + + protected StoragePoolDetailsDaoImpl() { + super(); + PoolSearch = createSearchBuilder(); + PoolSearch.and("pool", PoolSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + PoolSearch.done(); + } + + @Override + public void update(long poolId, Map details) { + Transaction txn = Transaction.currentTxn(); + SearchCriteria sc = PoolSearch.create(); + sc.setParameters("pool", poolId); + + txn.start(); + delete(sc); + for (Map.Entry entry : details.entrySet()) { + StoragePoolDetailVO detail = new StoragePoolDetailVO(poolId, entry.getKey(), entry.getValue()); + persist(detail); + } + txn.commit(); + } + + @Override + public Map getDetails(long poolId) { + SearchCriteria sc = PoolSearch.create(); + sc.setParameters("pool", poolId); + + List details = listActiveBy(sc); + Map detailsMap = new HashMap(); + for (StoragePoolDetailVO detail : details) { + detailsMap.put(detail.getName(), detail.getValue()); + } + + return detailsMap; + } +} diff --git a/core/src/com/cloud/storage/dao/StoragePoolHostDao.java b/core/src/com/cloud/storage/dao/StoragePoolHostDao.java new file mode 100644 index 00000000000..0e5ffc126e4 --- /dev/null +++ b/core/src/com/cloud/storage/dao/StoragePoolHostDao.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.ArrayList; +import java.util.List; + +import com.cloud.host.Status; +import com.cloud.storage.StoragePoolHostVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.Transaction; + +public interface StoragePoolHostDao extends GenericDao { + public List listByPoolId(long id); + + public List listByHostId(long hostId); + + public StoragePoolHostVO findByPoolHost(long poolId, long hostId); + + List listByHostStatus(long poolId, Status hostStatus); + + List> getDatacenterStoragePoolHostInfo(long dcId, boolean sharedOnly); + + public ArrayList getPoolIds(Long hostId); + + public void deletePrimaryRecordsForHost(long hostId); + + public void deleteStoragePoolHostDetails(long hostId, long poolId); +} diff --git a/core/src/com/cloud/storage/dao/StoragePoolHostDaoImpl.java b/core/src/com/cloud/storage/dao/StoragePoolHostDaoImpl.java new file mode 100644 index 00000000000..35891cb95ca --- /dev/null +++ b/core/src/com/cloud/storage/dao/StoragePoolHostDaoImpl.java @@ -0,0 +1,243 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.host.Status; +import com.cloud.storage.StoragePoolHostVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value={StoragePoolHostDao.class}) +public class StoragePoolHostDaoImpl extends GenericDaoBase implements StoragePoolHostDao { + public static final Logger s_logger = Logger.getLogger(StoragePoolHostDaoImpl.class.getName()); + + protected final SearchBuilder PoolSearch; + protected final SearchBuilder HostSearch; + protected final SearchBuilder PoolHostSearch; + + protected static final String HOST_FOR_POOL_SEARCH= + "SELECT * FROM storage_pool_host_ref ph, host h where ph.host_id = h.id and ph.pool_id=? and h.status=? "; + + protected static final String STORAGE_POOL_HOST_INFO = + "SELECT p.data_center_id, count(ph.host_id) " + + " FROM storage_pool p, storage_pool_host_ref ph " + + " WHERE p.id = ph.pool_id AND p.data_center_id = ? " + + " GROUP by p.data_center_id"; + + protected static final String SHARED_STORAGE_POOL_HOST_INFO = + "SELECT p.data_center_id, count(ph.host_id) " + + " FROM storage_pool p, storage_pool_host_ref ph " + + " WHERE p.id = ph.pool_id AND p.data_center_id = ? " + + " AND p.pool_type NOT IN ('LVM', 'Filesystem')" + + " GROUP by p.data_center_id"; + + protected static final String GET_POOL_IDS = + "SELECT pool_id "+ + "FROM storage_pool_host_ref "+ + "WHERE host_id = ?"; + + protected static final String DELETE_PRIMARY_RECORDS = + "DELETE "+ + "FROM storage_pool_host_ref "+ + "WHERE host_id = ?"; + + public StoragePoolHostDaoImpl () { + PoolSearch = createSearchBuilder(); + PoolSearch.and("pool_id", PoolSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + PoolSearch.done(); + + HostSearch = createSearchBuilder(); + HostSearch.and("host_id", HostSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostSearch.done(); + + PoolHostSearch = createSearchBuilder(); + PoolHostSearch.and("pool_id", PoolHostSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + PoolHostSearch.and("host_id", PoolHostSearch.entity().getHostId(), SearchCriteria.Op.EQ); + PoolHostSearch.done(); + + } + + @Override + public List listByPoolId(long id) { + SearchCriteria sc = PoolSearch.create(); + sc.setParameters("pool_id", id); + return listBy(sc); + } + + @Override + public List listByHostId(long hostId) { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("host_id", hostId); + return listBy(sc); + } + + @Override + public StoragePoolHostVO findByPoolHost(long poolId, long hostId) { + SearchCriteria sc = PoolHostSearch.create(); + sc.setParameters("pool_id", poolId); + sc.setParameters("host_id", hostId); + return findOneBy(sc); + } + + @Override + public List listByHostStatus(long poolId, Status hostStatus) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + List result = new ArrayList(); + ResultSet rs = null; + try { + String sql = HOST_FOR_POOL_SEARCH; + pstmt = txn.prepareStatement(sql); + + pstmt.setLong(1, poolId); + pstmt.setString(2, hostStatus.toString()); + rs = pstmt.executeQuery(); + while (rs.next()) { + // result.add(toEntityBean(rs, false)); TODO: this is buggy in GenericDaoBase for hand constructed queries + long id = rs.getLong(1); //ID column + result.add(findById(id)); + } + } catch (Exception e) { + s_logger.warn("Exception: ", e); + } finally { + try { + if (rs != null) { + rs.close(); + } + if (pstmt != null) { + pstmt.close(); + } + } catch (SQLException e) { + } + } + return result; + + } + + @Override + public List> getDatacenterStoragePoolHostInfo(long dcId, boolean sharedOnly) { + ArrayList> l = new ArrayList>(); + String sql = sharedOnly?SHARED_STORAGE_POOL_HOST_INFO:STORAGE_POOL_HOST_INFO; + Transaction txn = Transaction.currentTxn();; + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, dcId); + + ResultSet rs = pstmt.executeQuery(); + while(rs.next()) { + l.add(new Pair(rs.getLong(1), rs.getInt(2))); + } + } catch (SQLException e) { + } catch (Throwable e) { + } + return l; + } + + /** + * This method returns the pool_ids associated with the host + * @param hostId -- id for the host + * @return -- list of pool ids + */ + @DB + public ArrayList getPoolIds(Long hostId) + { + ArrayList poolIdsList = new ArrayList(); + + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + ResultSet rs = null; + try + { + String sql = GET_POOL_IDS; + pstmt = txn.prepareStatement(sql); + + pstmt.setLong(1, hostId); + rs = pstmt.executeQuery(); + while (rs.next()) + { + poolIdsList.add(rs.getLong(1)); + } + } + catch (Exception e) + { + s_logger.warn("Exception getting pool ids: ", e); + } + finally + { + try + { + if (rs != null) + { + rs.close(); + } + if (pstmt != null) + { + pstmt.close(); + } + } + catch (SQLException e) + { + } + } + + return poolIdsList; + } + + /** + * This method deletes the primary records from the host + * @param hostId -- id of the host + */ + public void deletePrimaryRecordsForHost(long hostId) + { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("host_id", hostId); + Transaction txn = Transaction.currentTxn(); + txn.start(); + remove(sc); + txn.commit(); + } + + + + @Override + public void deleteStoragePoolHostDetails(long hostId, long poolId) { + SearchCriteria sc = PoolHostSearch.create(); + sc.setParameters("host_id", hostId); + sc.setParameters("pool_id", poolId); + Transaction txn = Transaction.currentTxn(); + txn.start(); + remove(sc); + txn.commit(); + } +} diff --git a/core/src/com/cloud/storage/dao/VMTemplateDao.java b/core/src/com/cloud/storage/dao/VMTemplateDao.java new file mode 100644 index 00000000000..05e6816a0f6 --- /dev/null +++ b/core/src/com/cloud/storage/dao/VMTemplateDao.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.domain.DomainVO; +import com.cloud.storage.VMTemplateHostVO; +import com.cloud.storage.VMTemplateVO; +import com.cloud.user.Account; +import com.cloud.utils.db.GenericDao; + +/* + * Data Access Object for vm_templates table + */ +public interface VMTemplateDao extends GenericDao { + + public enum TemplateFilter { + featured, // returns templates that have been marked as featured and public + self, // returns templates that have been registered or created by the calling user + selfexecutable, // same as self, but only returns templates that are ready to be deployed with + sharedexecutable, // ready templates that have been granted to the calling user by another user + executable, // templates that are owned by the calling user, or public templates, that can be used to deploy a new VM + community, // returns templates that have been marked as public but not featured + all // all templates (only usable by ROOT admins) + } + + public List listByPublic(); + //finds by the column "unique_name" + public VMTemplateVO findByName(String templateName); + //finds by the column "name" + public VMTemplateVO findByTemplateName(String templateName); + //public void update(VMTemplateVO template); + public VMTemplateVO findRoutingTemplate(); + public VMTemplateVO findConsoleProxyTemplate(); + public VMTemplateVO findDefaultBuiltinTemplate(); + public String getRoutingTemplateUniqueName(); + public List findIsosByIdAndPath(Long domainId, Long accountId, String path); + public List listReadyTemplates(); + public List listByAccountId(long accountId); + public List searchTemplates(String name, String keyword, TemplateFilter templateFilter, boolean isIso, Boolean bootable, Account account, DomainVO domain, Integer pageSize, Long startIndex, Long zoneId); + + public long addTemplateToZone(VMTemplateVO tmplt, long zoneId); + public List listAllInZone(long dataCenterId); + +} diff --git a/core/src/com/cloud/storage/dao/VMTemplateDaoImpl.java b/core/src/com/cloud/storage/dao/VMTemplateDaoImpl.java new file mode 100644 index 00000000000..a02d33bdb53 --- /dev/null +++ b/core/src/com/cloud/storage/dao/VMTemplateDaoImpl.java @@ -0,0 +1,337 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.domain.DomainVO; +import com.cloud.storage.Storage; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VMTemplateZoneVO; +import com.cloud.storage.template.TemplateConstants; +import com.cloud.user.Account; +import com.cloud.utils.component.Inject; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.exception.CloudRuntimeException; + +@Local(value={VMTemplateDao.class}) +public class VMTemplateDaoImpl extends GenericDaoBase implements VMTemplateDao { + private static final Logger s_logger = Logger.getLogger(VMTemplateDaoImpl.class); + + @Inject + VMTemplateZoneDao _templateZoneDao; + + private final String SELECT_ALL = "SELECT t.id, t.unique_name, t.name, t.public, t.featured, t.type, t.hvm, t.bits, t.url, t.format, t.created, t.account_id, " + + "t.checksum, t.display_text, t.enable_password, t.guest_os_id, t.bootable, t.prepopulate, t.cross_zones FROM vm_template t"; + + protected static final String SELECT_ALL_IN_ZONE = + "SELECT t.id, t.unique_name, t.name, t.public, t.featured, t.type, t.hvm, t.bits, t.url, t.format, t.created, t.removed, t.account_id, " + + "t.checksum, t.display_text, t.enable_password, t.guest_os_id, t.bootable, t.prepopulate, t.cross_zones FROM vm_template t, template_zone_ref tz where t.removed is null and tz.removed is null and t.id = tz.template_id and tz.zone_id=? "; + + protected SearchBuilder TemplateNameSearch; + protected SearchBuilder UniqueNameSearch; + protected SearchBuilder AccountIdSearch; + protected SearchBuilder NameSearch; + + protected SearchBuilder PublicSearch; + private String routerTmpltName; + private String consoleProxyTmpltName; + + protected VMTemplateDaoImpl() { + } + + public List listByPublic() { + SearchCriteria sc = PublicSearch.create(); + sc.setParameters("public", 1); + return listActiveBy(sc); + } + + @Override + public VMTemplateVO findByName(String templateName) { + SearchCriteria sc = UniqueNameSearch.create(); + sc.setParameters("uniqueName", templateName); + return findOneBy(sc); + } + + @Override + public VMTemplateVO findByTemplateName(String templateName) { + SearchCriteria sc = NameSearch.create(); + sc.setParameters("name", templateName); + return findOneBy(sc); + } + + @Override + public VMTemplateVO findRoutingTemplate() { + SearchCriteria sc = UniqueNameSearch.create(); + sc.setParameters("uniqueName", routerTmpltName); + return findOneBy(sc); + } + + @Override + public VMTemplateVO findConsoleProxyTemplate() { + SearchCriteria sc = UniqueNameSearch.create(); + sc.setParameters("uniqueName", consoleProxyTmpltName); + return findOneBy(sc); + } + + @Override + public List listReadyTemplates() { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("ready", SearchCriteria.Op.EQ, true); + sc.addAnd("format", SearchCriteria.Op.NEQ, Storage.ImageFormat.ISO); + return listBy(sc); + } + + @Override + public List findIsosByIdAndPath(Long domainId, Long accountId, String path) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("iso", SearchCriteria.Op.EQ, true); + if (domainId != null) + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + if (accountId != null) + sc.addAnd("accountId", SearchCriteria.Op.EQ, accountId); + if (path != null) + sc.addAnd("path", SearchCriteria.Op.EQ, path); + return listBy(sc); + } + + @Override + public List listByAccountId(long accountId) { + SearchCriteria sc = AccountIdSearch.create(); + sc.setParameters("accountId", accountId); + return listActiveBy(sc); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + boolean result = super.configure(name, params); + + PublicSearch = createSearchBuilder(); + PublicSearch.and("public", PublicSearch.entity().isPublicTemplate(), SearchCriteria.Op.EQ); + + routerTmpltName = (String)params.get("routing.uniquename"); + + s_logger.debug("Found parameter routing unique name " + routerTmpltName); + if (routerTmpltName==null) { + routerTmpltName="routing"; + } + + consoleProxyTmpltName = (String)params.get("consoleproxy.uniquename"); + if(consoleProxyTmpltName == null) + consoleProxyTmpltName = "routing"; + if(s_logger.isDebugEnabled()) + s_logger.debug("Use console proxy template : " + consoleProxyTmpltName); + + TemplateNameSearch = createSearchBuilder(); + TemplateNameSearch.and("name", TemplateNameSearch.entity().getName(), SearchCriteria.Op.EQ); + UniqueNameSearch = createSearchBuilder(); + UniqueNameSearch.and("uniqueName", UniqueNameSearch.entity().getUniqueName(), SearchCriteria.Op.EQ); + NameSearch = createSearchBuilder(); + NameSearch.and("name", NameSearch.entity().getName(), SearchCriteria.Op.EQ); + + AccountIdSearch = createSearchBuilder(); + AccountIdSearch.and("accountId", AccountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountIdSearch.and("publicTemplate", AccountIdSearch.entity().isPublicTemplate(), SearchCriteria.Op.EQ); + AccountIdSearch.done(); + + return result; + } + + @Override + public String getRoutingTemplateUniqueName() { + return routerTmpltName; + } + + @Override + public List searchTemplates(String name, String keyword, TemplateFilter templateFilter, boolean isIso, Boolean bootable, Account account, DomainVO domain, Integer pageSize, Long startIndex, Long zoneId) { + Transaction txn = Transaction.currentTxn(); + txn.start(); + List templates = new ArrayList(); + PreparedStatement pstmt = null; + ResultSet rs = null; + try { + short accountType; + String accountId = null; + if (account != null) { + accountType = account.getType(); + accountId = account.getId().toString(); + } else { + accountType = Account.ACCOUNT_TYPE_ADMIN; + } + + String sql = SELECT_ALL; + String whereClause = ""; + + if (templateFilter == TemplateFilter.featured) { + whereClause += " WHERE t.public = 1 AND t.featured = 1"; + } else if ((templateFilter == TemplateFilter.self || templateFilter == TemplateFilter.selfexecutable) && accountType != Account.ACCOUNT_TYPE_ADMIN) { + if (accountType == Account.ACCOUNT_TYPE_DOMAIN_ADMIN) { + whereClause += " INNER JOIN account a on (t.account_id = a.id) INNER JOIN domain d on (a.domain_id = d.id) WHERE d.path LIKE '" + domain.getPath() + "%'"; + } else { + whereClause += " WHERE t.account_id = " + accountId; + } + } else if (templateFilter == TemplateFilter.sharedexecutable && accountType != Account.ACCOUNT_TYPE_ADMIN) { + if (accountType == Account.ACCOUNT_TYPE_NORMAL) { + whereClause += " LEFT JOIN launch_permission lp ON t.id = lp.template_id WHERE" + + " (t.account_id = " + accountId + " OR" + + " lp.account_id = " + accountId + ")"; + } else { + whereClause += " INNER JOIN account a on (t.account_id = a.id) INNER JOIN domain d on (a.domain_id = d.id) WHERE d.path LIKE '" + domain.getPath() + "%'"; + } + } else if (templateFilter == TemplateFilter.executable && accountId != null) { + whereClause += " WHERE (t.public = 1 OR t.account_id = " + accountId + ")"; + } else if (templateFilter == TemplateFilter.community) { + whereClause += " WHERE t.public = 1 AND t.featured = 0"; + } else if (templateFilter == TemplateFilter.all && accountType == Account.ACCOUNT_TYPE_ADMIN) { + whereClause += " WHERE "; + } else if (accountType != Account.ACCOUNT_TYPE_ADMIN) { + return templates; + } + + if (whereClause.equals("")) { + whereClause += " WHERE "; + } else if (!whereClause.equals(" WHERE ")) { + whereClause += " AND "; + } + + sql += whereClause + getExtrasWhere(templateFilter, name, keyword, isIso, bootable) + getOrderByLimit(pageSize, startIndex); + + pstmt = txn.prepareStatement(sql); + rs = pstmt.executeQuery(); + while (rs.next()) { + VMTemplateVO tmplt = toEntityBean(rs, false); + if (zoneId != null) { + VMTemplateZoneVO vtzvo = _templateZoneDao.findByZoneTemplate(zoneId, tmplt.getId()); + if (vtzvo != null){ + templates.add(tmplt); + } + } else { + templates.add(tmplt); + } + } + } catch (Exception e) { + s_logger.warn("Error listing templates", e); + } finally { + try { + if (rs != null) { + rs.close(); + } + if (pstmt != null) { + pstmt.close(); + } + txn.commit(); + } catch( SQLException sqle) { + s_logger.warn("Error in cleaning up", sqle); + } + } + + return templates; + } + + private String getExtrasWhere(TemplateFilter templateFilter, String name, String keyword, boolean isIso, Boolean bootable) { + String sql = ""; + if (keyword != null) { + sql += " t.name LIKE \"%" + keyword + "%\" AND"; + } else if (name != null) { + sql += " t.name LIKE \"%" + name + "%\" AND"; + } + + if (isIso) { + sql += " t.format = 'ISO'"; + } else { + sql += " t.format <> 'ISO'"; + } + + if (bootable != null) { + sql += " AND t.bootable = " + bootable; + } + + sql += " AND t.removed IS NULL"; + + return sql; + } + + private String getOrderByLimit(Integer pageSize, Long startIndex) { + String sql = " ORDER BY t.created DESC"; + if ((pageSize != null) && (startIndex != null)) { + sql += " LIMIT " + startIndex.toString() + "," + pageSize.toString(); + } + return sql; + } + + @Override + @DB + public long addTemplateToZone(VMTemplateVO tmplt, long zoneId) { + Transaction txn = Transaction.currentTxn(); + txn.start(); + VMTemplateVO tmplt2 = findById(tmplt.getId()); + if (tmplt2 == null){ + persist(tmplt); + } + VMTemplateZoneVO tmpltZoneVO = new VMTemplateZoneVO(zoneId, tmplt.getId(), new Date()); + _templateZoneDao.persist(tmpltZoneVO); + txn.commit(); + + return tmplt.getId(); + } + + @Override + @DB + public List listAllInZone(long dataCenterId) { + Transaction txn = Transaction.currentTxn(); + txn.start(); + PreparedStatement pstmt = null; + List result = new ArrayList(); + try { + String sql = SELECT_ALL_IN_ZONE; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, dataCenterId); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + result.add(toEntityBean(rs, false)); + } + txn.commit(); + } catch (SQLException sqle) { + s_logger.warn("Exception: ",sqle); + throw new CloudRuntimeException("Unable to list templates in zone", sqle); + } + + return result; + } + + @Override + public VMTemplateVO findDefaultBuiltinTemplate() { + return findById(TemplateConstants.DEFAULT_BUILTIN_VM_DB_ID); + } +} diff --git a/core/src/com/cloud/storage/dao/VMTemplateHostDao.java b/core/src/com/cloud/storage/dao/VMTemplateHostDao.java new file mode 100644 index 00000000000..5a2b26cd5c2 --- /dev/null +++ b/core/src/com/cloud/storage/dao/VMTemplateHostDao.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General License for more details. + * + * You should have received a copy of the GNU General License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.VMTemplateHostVO; +import com.cloud.utils.db.GenericDao; + +public interface VMTemplateHostDao extends GenericDao { + List listByHostId(long id); + + List listByTemplateId(long templateId); + + List listByOnlyTemplateId(long templateId); + + VMTemplateHostVO findByHostTemplate(long hostId, long templateId); + + VMTemplateHostVO findByHostTemplate(long hostId, long templateId, boolean lock); + + List listByHostTemplate(long hostId, long templateId); + + VMTemplateHostVO findByHostTemplatePool(long hostId, long templateId, long poolId); + + List listByTemplatePool(long templateId, long poolId); + + void update(VMTemplateHostVO instance); + + List listByTemplateStatus(long templateId, VMTemplateHostVO.Status downloadState); + + List listByTemplateStatus(long templateId, long datacenterId, VMTemplateHostVO.Status downloadState); + + List listByTemplateStatus(long templateId, long datacenterId, long podId, VMTemplateHostVO.Status downloadState); + + List listByTemplateStates(long templateId, VMTemplateHostVO.Status... states); + + List listDestroyed(long hostId); + + boolean templateAvailable(long templateId, long hostId); +} diff --git a/core/src/com/cloud/storage/dao/VMTemplateHostDaoImpl.java b/core/src/com/cloud/storage/dao/VMTemplateHostDaoImpl.java new file mode 100644 index 00000000000..48f3d939624 --- /dev/null +++ b/core/src/com/cloud/storage/dao/VMTemplateHostDaoImpl.java @@ -0,0 +1,293 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.storage.VMTemplateHostVO; +import com.cloud.storage.VMTemplateStorageResourceAssoc.Status; +import com.cloud.utils.DateUtil; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value={VMTemplateHostDao.class}) +public class VMTemplateHostDaoImpl extends GenericDaoBase implements VMTemplateHostDao { + public static final Logger s_logger = Logger.getLogger(VMTemplateHostDaoImpl.class.getName()); + + protected final SearchBuilder HostSearch; + protected final SearchBuilder TemplateSearch; + protected final SearchBuilder HostTemplateSearch; + protected final SearchBuilder HostDestroyedSearch; + protected final SearchBuilder PoolTemplateSearch; + protected final SearchBuilder HostTemplatePoolSearch; + protected final SearchBuilder TemplateStatusSearch; + protected final SearchBuilder TemplateStatesSearch; + + protected static final String UPDATE_TEMPLATE_HOST_REF = + "UPDATE template_host_ref SET download_state = ?, download_pct= ?, last_updated = ? " + + ", error_str = ?, local_path = ?, job_id = ? " + + "WHERE host_id = ? and template_id = ?"; + + protected static final String DOWNLOADS_STATE_DC= + "SELECT * FROM template_host_ref t, host h where t.host_id = h.id and h.data_center_id=? " + + " and t.template_id=? and t.download_state = ?" ; + + protected static final String DOWNLOADS_STATE_DC_POD= + "SELECT * FROM template_host_ref t, host h where t.host_id = h.id and h.data_center_id=? and h.pod_id=? " + + " and t.template_id=? and t.download_state=?" ; + + protected static final String DOWNLOADS_STATE= + "SELECT * FROM template_host_ref t " + + " where t.template_id=? and t.download_state=?"; + + public VMTemplateHostDaoImpl () { + HostSearch = createSearchBuilder(); + HostSearch.and("host_id", HostSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostSearch.done(); + + TemplateSearch = createSearchBuilder(); + TemplateSearch.and("template_id", TemplateSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateSearch.and("destroyed", TemplateSearch.entity().getDestroyed(), SearchCriteria.Op.EQ); + TemplateSearch.done(); + + HostTemplateSearch = createSearchBuilder(); + HostTemplateSearch.and("host_id", HostTemplateSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostTemplateSearch.and("template_id", HostTemplateSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + HostTemplateSearch.done(); + + HostDestroyedSearch = createSearchBuilder(); + HostDestroyedSearch.and("host_id", HostDestroyedSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostDestroyedSearch.and("destroyed", HostDestroyedSearch.entity().getDestroyed(), SearchCriteria.Op.EQ); + HostDestroyedSearch.done(); + + HostTemplatePoolSearch = createSearchBuilder(); + HostTemplatePoolSearch.and("host_id", HostTemplatePoolSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostTemplatePoolSearch.and("template_id", HostTemplatePoolSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + HostTemplatePoolSearch.and("pool_id", HostTemplatePoolSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + HostTemplatePoolSearch.done(); + + TemplateStatusSearch = createSearchBuilder(); + TemplateStatusSearch.and("template_id", TemplateStatusSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateStatusSearch.and("download_state", TemplateStatusSearch.entity().getDownloadState(), SearchCriteria.Op.EQ); + TemplateStatusSearch.done(); + + TemplateStatesSearch = createSearchBuilder(); + TemplateStatesSearch.and("template_id", TemplateStatesSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateStatesSearch.and("states", TemplateStatesSearch.entity().getDownloadState(), SearchCriteria.Op.IN); + TemplateStatesSearch.done(); + + PoolTemplateSearch = createSearchBuilder(); + PoolTemplateSearch.and("pool_id", PoolTemplateSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + PoolTemplateSearch.and("template_id", PoolTemplateSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + PoolTemplateSearch.done(); + } + + public void update(VMTemplateHostVO instance) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + Date now = new Date(); + String sql = UPDATE_TEMPLATE_HOST_REF; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setString(1, instance.getDownloadState().toString()); + pstmt.setInt(2, instance.getDownloadPercent()); + pstmt.setString(3, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), now)); + pstmt.setString(4, instance.getErrorString()); + pstmt.setString(5, instance.getLocalDownloadPath()); + pstmt.setString(6, instance.getJobId()); + pstmt.setLong(7, instance.getHostId()); + pstmt.setLong(8, instance.getTemplateId()); + pstmt.executeUpdate(); + } catch (Exception e) { + s_logger.warn("Exception: ", e); + } + } + + @Override + public List listByHostId(long id) { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("host_id", id); + return listBy(sc); + } + + @Override + public List listByTemplateId(long templateId) { + SearchCriteria sc = TemplateSearch.create(); + sc.setParameters("template_id", templateId); + sc.setParameters("destroyed", false); + return listBy(sc); + } + + @Override + public List listByOnlyTemplateId(long templateId) { + SearchCriteria sc = TemplateSearch.create(); + sc.setParameters("template_id", templateId); + return listBy(sc); + } + + @Override + public VMTemplateHostVO findByHostTemplate(long hostId, long templateId) { + SearchCriteria sc = HostTemplateSearch.create(); + sc.setParameters("host_id", hostId); + sc.setParameters("template_id", templateId); + return findOneBy(sc); + } + + @Override + public List listByTemplateStatus(long templateId, VMTemplateHostVO.Status downloadState) { + SearchCriteria sc = TemplateStatusSearch.create(); + sc.setParameters("template_id", templateId); + sc.setParameters("download_state", downloadState.toString()); + return listBy(sc); + } + + @Override + public List listByTemplateStatus(long templateId, long datacenterId, VMTemplateHostVO.Status downloadState) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + List result = new ArrayList(); + try { + String sql = DOWNLOADS_STATE_DC; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, datacenterId); + pstmt.setLong(2, templateId); + pstmt.setString(3, downloadState.toString()); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + result.add(toEntityBean(rs, false)); + } + } catch (Exception e) { + s_logger.warn("Exception: ", e); + } + return result; + + } + + @Override + public List listByTemplateStatus(long templateId, long datacenterId, long podId, VMTemplateHostVO.Status downloadState) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + List result = new ArrayList(); + ResultSet rs = null; + try { + String sql = DOWNLOADS_STATE_DC_POD; + pstmt = txn.prepareStatement(sql); + + pstmt.setLong(1, datacenterId); + pstmt.setLong(2, podId); + pstmt.setLong(3, templateId); + pstmt.setString(4, downloadState.toString()); + rs = pstmt.executeQuery(); + while (rs.next()) { + // result.add(toEntityBean(rs, false)); TODO: this is buggy in GenericDaoBase for hand constructed queries + long id = rs.getLong(1); //ID column + result.add(findById(id)); + } + } catch (Exception e) { + s_logger.warn("Exception: ", e); + } finally { + try { + if (rs != null) { + rs.close(); + } + if (pstmt != null) { + pstmt.close(); + } + } catch (SQLException e) { + } + } + return result; + + } + + @Override + public boolean templateAvailable(long templateId, long hostId) { + VMTemplateHostVO tmpltHost = findByHostTemplate(hostId, templateId); + if (tmpltHost == null) + return false; + + return tmpltHost.getDownloadState()==Status.DOWNLOADED; + } + + @Override + public List listByTemplateStates(long templateId, VMTemplateHostVO.Status... states) { + SearchCriteria sc = TemplateStatesSearch.create(); + sc.setParameters("states", (Object[])states); + sc.setParameters("template_id", templateId); + + return search(sc, null); + } + + @Override + public VMTemplateHostVO findByHostTemplatePool(long hostId, long templateId, long poolId) { + SearchCriteria sc = HostTemplatePoolSearch.create(); + sc.setParameters("host_id", hostId); + sc.setParameters("template_id", templateId); + sc.setParameters("pool_id", poolId); + return findOneBy(sc); + } + + @Override + public List listByHostTemplate(long hostId, long templateId) { + SearchCriteria sc = HostTemplateSearch.create(); + sc.setParameters("host_id", hostId); + sc.setParameters("template_id", templateId); + return listBy(sc); + } + + @Override + public List listByTemplatePool(long templateId, long poolId) { + SearchCriteria sc = PoolTemplateSearch.create(); + sc.setParameters("pool_id", poolId); + sc.setParameters("template_id", templateId); + return listBy(sc); + } + + @Override + public List listDestroyed(long hostId) { + SearchCriteria sc = HostDestroyedSearch.create(); + sc.setParameters("host_id", hostId); + sc.setParameters("destroyed", true); + return listBy(sc); + } + + @Override + public VMTemplateHostVO findByHostTemplate(long hostId, long templateId, boolean lock) { + SearchCriteria sc = HostTemplateSearch.create(); + sc.setParameters("host_id", hostId); + sc.setParameters("template_id", templateId); + if (!lock) + return findOneBy(sc); + else + return lock(sc, true); + } + +} diff --git a/core/src/com/cloud/storage/dao/VMTemplatePoolDao.java b/core/src/com/cloud/storage/dao/VMTemplatePoolDao.java new file mode 100644 index 00000000000..953af029974 --- /dev/null +++ b/core/src/com/cloud/storage/dao/VMTemplatePoolDao.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.utils.db.GenericDao; + +public interface VMTemplatePoolDao extends GenericDao { + public List listByPoolId(long id); + + public List listByTemplateId(long templateId); + + public VMTemplateStoragePoolVO findByPoolTemplate(long poolId, long templateId); + + public List listByTemplateStatus(long templateId, VMTemplateStoragePoolVO.Status downloadState); + + public List listByTemplateStatus(long templateId, VMTemplateStoragePoolVO.Status downloadState, long poolId); + + public List listByTemplateStatus(long templateId, long datacenterId, VMTemplateStoragePoolVO.Status downloadState); + + public List listByTemplateStatus(long templateId, long datacenterId, long podId, VMTemplateStoragePoolVO.Status downloadState); + + public List listByTemplateStates(long templateId, VMTemplateStoragePoolVO.Status ... states); + + + boolean templateAvailable(long templateId, long poolId); + + public VMTemplateStoragePoolVO findByHostTemplate(Long hostId, Long templateId); + +} diff --git a/core/src/com/cloud/storage/dao/VMTemplatePoolDaoImpl.java b/core/src/com/cloud/storage/dao/VMTemplatePoolDaoImpl.java new file mode 100644 index 00000000000..e19ca03b170 --- /dev/null +++ b/core/src/com/cloud/storage/dao/VMTemplatePoolDaoImpl.java @@ -0,0 +1,255 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.storage.VMTemplateStorageResourceAssoc; +import com.cloud.storage.VMTemplateStorageResourceAssoc.Status; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value={VMTemplatePoolDao.class}) +public class VMTemplatePoolDaoImpl extends GenericDaoBase implements VMTemplatePoolDao { + public static final Logger s_logger = Logger.getLogger(VMTemplatePoolDaoImpl.class.getName()); + + protected final SearchBuilder PoolSearch; + protected final SearchBuilder TemplateSearch; + protected final SearchBuilder PoolTemplateSearch; + protected final SearchBuilder TemplateStatusSearch; + protected final SearchBuilder TemplatePoolStatusSearch; + protected final SearchBuilder TemplateStatesSearch; + + protected static final String UPDATE_TEMPLATE_HOST_REF = + "UPDATE template_spool_ref SET download_state = ?, download_pct= ?, last_updated = ? " + + ", error_str = ?, local_path = ?, job_id = ? " + + "WHERE pool_id = ? and template_id = ?"; + + protected static final String DOWNLOADS_STATE_DC= + "SELECT * FROM template_spool_ref t, storage_pool p where t.pool_id = p.id and p.data_center_id=? " + + " and t.template_id=? and t.download_state = ?" ; + + protected static final String DOWNLOADS_STATE_DC_POD= + "SELECT * FROM template_spool_ref tp, storage_pool_host_ref ph, host h where tp.pool_id = ph.pool_id and ph.host_id = h.id and h.data_center_id=? and h.pod_id=? " + + " and tp.template_id=? and tp.download_state=?" ; + + protected static final String HOST_TEMPLATE_SEARCH= + "SELECT * FROM template_spool_ref tp, storage_pool_host_ref ph, host h where tp.pool_id = ph.pool_id and ph.host_id = h.id and h.id=? " + + " and tp.template_id=? " ; + + + public VMTemplatePoolDaoImpl () { + PoolSearch = createSearchBuilder(); + PoolSearch.and("pool_id", PoolSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + PoolSearch.done(); + + TemplateSearch = createSearchBuilder(); + TemplateSearch.and("template_id", TemplateSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateSearch.done(); + + PoolTemplateSearch = createSearchBuilder(); + PoolTemplateSearch.and("pool_id", PoolTemplateSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + PoolTemplateSearch.and("template_id", PoolTemplateSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + PoolTemplateSearch.done(); + + TemplateStatusSearch = createSearchBuilder(); + TemplateStatusSearch.and("template_id", TemplateStatusSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateStatusSearch.and("download_state", TemplateStatusSearch.entity().getDownloadState(), SearchCriteria.Op.EQ); + TemplateStatusSearch.done(); + + TemplatePoolStatusSearch = createSearchBuilder(); + TemplatePoolStatusSearch.and("pool_id", TemplatePoolStatusSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + TemplatePoolStatusSearch.and("template_id", TemplatePoolStatusSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplatePoolStatusSearch.and("download_state", TemplatePoolStatusSearch.entity().getDownloadState(), SearchCriteria.Op.EQ); + TemplatePoolStatusSearch.done(); + + TemplateStatesSearch = createSearchBuilder(); + TemplateStatesSearch.and("template_id", TemplateStatesSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateStatesSearch.and("states", TemplateStatesSearch.entity().getDownloadState(), SearchCriteria.Op.IN); + TemplateStatesSearch.done(); + } + + @Override + public List listByPoolId(long id) { + SearchCriteria sc = PoolSearch.create(); + sc.setParameters("pool_id", id); + return listBy(sc); + } + + @Override + public List listByTemplateId(long templateId) { + SearchCriteria sc = TemplateSearch.create(); + sc.setParameters("template_id", templateId); + return listBy(sc); + } + + @Override + public VMTemplateStoragePoolVO findByPoolTemplate(long hostId, long templateId) { + SearchCriteria sc = PoolTemplateSearch.create(); + sc.setParameters("pool_id", hostId); + sc.setParameters("template_id", templateId); + return findOneBy(sc); + } + + @Override + public List listByTemplateStatus(long templateId, VMTemplateStoragePoolVO.Status downloadState) { + SearchCriteria sc = TemplateStatusSearch.create(); + sc.setParameters("template_id", templateId); + sc.setParameters("download_state", downloadState.toString()); + return listBy(sc); + } + + @Override + public List listByTemplateStatus(long templateId, VMTemplateStoragePoolVO.Status downloadState, long poolId) { + SearchCriteria sc = TemplatePoolStatusSearch.create(); + sc.setParameters("pool_id", poolId); + sc.setParameters("template_id", templateId); + sc.setParameters("download_state", downloadState.toString()); + return listBy(sc); + } + + @Override + public List listByTemplateStatus(long templateId, long datacenterId, VMTemplateStoragePoolVO.Status downloadState) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + List result = new ArrayList(); + try { + String sql = DOWNLOADS_STATE_DC; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, datacenterId); + pstmt.setLong(2, templateId); + pstmt.setString(3, downloadState.toString()); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + result.add(toEntityBean(rs, false)); + } + } catch (Exception e) { + s_logger.warn("Exception: ", e); + } + return result; + + } + + @Override + public List listByTemplateStatus(long templateId, long datacenterId, long podId, VMTemplateStoragePoolVO.Status downloadState) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + List result = new ArrayList(); + ResultSet rs = null; + try { + String sql = DOWNLOADS_STATE_DC_POD; + pstmt = txn.prepareStatement(sql); + + pstmt.setLong(1, datacenterId); + pstmt.setLong(2, podId); + pstmt.setLong(3, templateId); + pstmt.setString(4, downloadState.toString()); + rs = pstmt.executeQuery(); + while (rs.next()) { + // result.add(toEntityBean(rs, false)); TODO: this is buggy in GenericDaoBase for hand constructed queries + long id = rs.getLong(1); //ID column + result.add(findById(id)); + } + } catch (Exception e) { + s_logger.warn("Exception: ", e); + } finally { + try { + if (rs != null) { + rs.close(); + } + if (pstmt != null) { + pstmt.close(); + } + } catch (SQLException e) { + } + } + return result; + + } + + public List listByHostTemplate(long hostId, long templateId) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + List result = new ArrayList(); + ResultSet rs = null; + try { + String sql = HOST_TEMPLATE_SEARCH; + pstmt = txn.prepareStatement(sql); + + pstmt.setLong(1, hostId); + pstmt.setLong(2, templateId); + rs = pstmt.executeQuery(); + while (rs.next()) { + // result.add(toEntityBean(rs, false)); TODO: this is buggy in GenericDaoBase for hand constructed queries + long id = rs.getLong(1); //ID column + result.add(findById(id)); + } + } catch (Exception e) { + s_logger.warn("Exception: ", e); + } finally { + try { + if (rs != null) { + rs.close(); + } + if (pstmt != null) { + pstmt.close(); + } + } catch (SQLException e) { + } + } + return result; + + } + + @Override + public boolean templateAvailable(long templateId, long hostId) { + VMTemplateStorageResourceAssoc tmpltPool = findByPoolTemplate(hostId, templateId); + if (tmpltPool == null) + return false; + + return tmpltPool.getDownloadState()==Status.DOWNLOADED; + } + + @Override + public List listByTemplateStates(long templateId, VMTemplateStoragePoolVO.Status... states) { + SearchCriteria sc = TemplateStatesSearch.create(); + sc.setParameters("states", (Object[])states); + sc.setParameters("template_id", templateId); + + return search(sc, null); + } + + @Override + public VMTemplateStoragePoolVO findByHostTemplate(Long hostId, Long templateId) { + List result = listByHostTemplate(hostId, templateId); + return (result.size() == 0)?null:result.get(1); + } + +} diff --git a/core/src/com/cloud/storage/dao/VMTemplateZoneDao.java b/core/src/com/cloud/storage/dao/VMTemplateZoneDao.java new file mode 100755 index 00000000000..a064cbbc421 --- /dev/null +++ b/core/src/com/cloud/storage/dao/VMTemplateZoneDao.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.VMTemplateZoneVO; +import com.cloud.utils.db.GenericDao; + +public interface VMTemplateZoneDao extends GenericDao { + public List listByZoneId(long id); + + public List listByTemplateId(long templateId); + + public VMTemplateZoneVO findByZoneTemplate(long zoneId, long templateId); + + public List listByZoneTemplate(long zoneId, long templateId); + +} diff --git a/core/src/com/cloud/storage/dao/VMTemplateZoneDaoImpl.java b/core/src/com/cloud/storage/dao/VMTemplateZoneDaoImpl.java new file mode 100644 index 00000000000..75f8cf8ad50 --- /dev/null +++ b/core/src/com/cloud/storage/dao/VMTemplateZoneDaoImpl.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.dao; + +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.storage.VMTemplateZoneVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={VMTemplateZoneDao.class}) +public class VMTemplateZoneDaoImpl extends GenericDaoBase implements VMTemplateZoneDao { + public static final Logger s_logger = Logger.getLogger(VMTemplateZoneDaoImpl.class.getName()); + + protected final SearchBuilder ZoneSearch; + protected final SearchBuilder TemplateSearch; + protected final SearchBuilder ZoneTemplateSearch; + + + public VMTemplateZoneDaoImpl () { + ZoneSearch = createSearchBuilder(); + ZoneSearch.and("zone_id", ZoneSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + ZoneSearch.done(); + + TemplateSearch = createSearchBuilder(); + TemplateSearch.and("template_id", TemplateSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateSearch.done(); + + ZoneTemplateSearch = createSearchBuilder(); + ZoneTemplateSearch.and("zone_id", ZoneTemplateSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + ZoneTemplateSearch.and("template_id", ZoneTemplateSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + ZoneTemplateSearch.done(); + } + + + @Override + public List listByZoneId(long id) { + SearchCriteria sc = ZoneSearch.create(); + sc.setParameters("zone_id", id); + return listBy(sc); + } + + @Override + public List listByTemplateId(long templateId) { + SearchCriteria sc = TemplateSearch.create(); + sc.setParameters("template_id", templateId); + return listBy(sc); + } + + @Override + public VMTemplateZoneVO findByZoneTemplate(long zoneId, long templateId) { + SearchCriteria sc = ZoneTemplateSearch.create(); + sc.setParameters("zone_id", zoneId); + sc.setParameters("template_id", templateId); + return findOneBy(sc); + } + + @Override + public List listByZoneTemplate(long zoneId, long templateId) { + SearchCriteria sc = ZoneTemplateSearch.create(); + sc.setParameters("zone_id", zoneId); + sc.setParameters("template_id", templateId); + return listBy(sc); + } + +} diff --git a/core/src/com/cloud/storage/dao/VolumeDao.java b/core/src/com/cloud/storage/dao/VolumeDao.java new file mode 100755 index 00000000000..39979744aab --- /dev/null +++ b/core/src/com/cloud/storage/dao/VolumeDao.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; + +public interface VolumeDao extends GenericDao { + List findDetachedByAccount(long accountId); + List findByAccount(long accountId); + Pair getCountAndTotalByPool(long poolId); + List findByInstance(long id); + List findByInstanceAndType(long id, Volume.VolumeType vType); + List findByInstanceIdDestroyed(long vmId); + List findByDetachedDestroyed(); + List findByAccountAndPod(long accountId, long podId); + List findByTemplateAndZone(long templateId, long zoneId); + List findVMInstancesByStorageHost(long hostId, Volume.MirrorState mState); + List findStrandedMirrorVolumes(); + List findVmsStoredOnHost(long hostId); + void deleteVolumesByInstance(long instanceId); + void attachVolume(long volumeId, long vmId, long deviceId); + void detachVolume(long volumeId); + void destroyVolume(long volumeId); + void recoverVolume(long volumeId); + boolean isAnyVolumeActivelyUsingTemplateOnPool(long templateId, long poolId); + List listRemovedButNotDestroyed(); + List findCreatedByInstance(long id); + List findByPoolId(long poolId); +} diff --git a/core/src/com/cloud/storage/dao/VolumeDaoImpl.java b/core/src/com/cloud/storage/dao/VolumeDaoImpl.java new file mode 100755 index 00000000000..976c6445483 --- /dev/null +++ b/core/src/com/cloud/storage/dao/VolumeDaoImpl.java @@ -0,0 +1,357 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.async.AsyncInstanceCreateStatus; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.Volume.MirrorState; +import com.cloud.storage.Volume.VolumeType; +import com.cloud.utils.Pair; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.SearchCriteria.Func; +import com.cloud.utils.db.SearchCriteria.Op; +import com.cloud.utils.exception.CloudRuntimeException; + +@Local(value=VolumeDao.class) @DB(txn=false) +public class VolumeDaoImpl extends GenericDaoBase implements VolumeDao { + private static final Logger s_logger = Logger.getLogger(VolumeDaoImpl.class); + protected final SearchBuilder DetachedAccountIdSearch; + protected final SearchBuilder AccountIdSearch; + protected final SearchBuilder AccountPodSearch; + protected final SearchBuilder TemplateZoneSearch; + protected final GenericSearchBuilder TotalSizeByPoolSearch; + protected final SearchBuilder InstanceIdSearch; + protected final SearchBuilder InstanceAndTypeSearch; + protected final SearchBuilder InstanceIdDestroyedSearch; + protected final SearchBuilder InstanceIdCreatedSearch; + protected final SearchBuilder DetachedDestroyedSearch; + protected final SearchBuilder MirrorSearch; + protected final GenericSearchBuilder ActiveTemplateSearch; + protected final SearchBuilder RemovedButNotDestroyedSearch; + protected final SearchBuilder PoolIdSearch; + + protected static final String SELECT_VM_SQL = "SELECT DISTINCT instance_id from volumes v where v.host_id = ? and v.mirror_state = ?"; + protected static final String SELECT_VM_ID_SQL = "SELECT DISTINCT instance_id from volumes v where v.host_id = ?"; + + @Override + public List listRemovedButNotDestroyed() { + SearchCriteria sc = RemovedButNotDestroyedSearch.create(); + sc.setParameters("destroyed", false); + + return searchAll(sc, null, null, false); + } + + @Override @DB + public List findVmsStoredOnHost(long hostId) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + List result = new ArrayList(); + + try { + String sql = SELECT_VM_ID_SQL; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, hostId); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + result.add(rs.getLong(1)); + } + return result; + } catch (SQLException e) { + throw new CloudRuntimeException("DB Exception on: " + SELECT_VM_SQL, e); + } catch (Throwable e) { + throw new CloudRuntimeException("Caught: " + SELECT_VM_SQL, e); + } + } + + @Override + public List findDetachedByAccount(long accountId) { + SearchCriteria sc = DetachedAccountIdSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("destroyed", false); + return listActiveBy(sc); + } + + @Override + public List findByAccount(long accountId) { + SearchCriteria sc = AccountIdSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("destroyed", false); + return listActiveBy(sc); + } + + @Override + public List findByInstance(long id) { + SearchCriteria sc = InstanceIdSearch.create(); + sc.setParameters("instanceId", id); + return listActiveBy(sc); + } + + @Override + public List findByPoolId(long poolId) { + SearchCriteria sc = PoolIdSearch.create(); + sc.setParameters("poolId", poolId); + return listActiveBy(sc); + } + + @Override + public List findCreatedByInstance(long id) { + SearchCriteria sc = InstanceIdCreatedSearch.create(); + sc.setParameters("instanceId", id); + sc.setParameters("status", AsyncInstanceCreateStatus.Created); + sc.setParameters("destroyed", false); + return listActiveBy(sc); + } + + @Override + public List findByInstanceAndType(long id, VolumeType vType) { + SearchCriteria sc = InstanceAndTypeSearch.create(); + sc.setParameters("instanceId", id); + sc.setParameters("vType", vType.toString()); + return listActiveBy(sc); + } + + @Override + public List findByInstanceIdDestroyed(long vmId) { + SearchCriteria sc = InstanceIdDestroyedSearch.create(); + sc.setParameters("instanceId", vmId); + sc.setParameters("destroyed", true); + return listBy(sc); + } + + @Override + public List findByDetachedDestroyed() { + SearchCriteria sc = DetachedDestroyedSearch.create(); + sc.setParameters("destroyed", true); + return listActiveBy(sc); + } + + @Override + public List findByAccountAndPod(long accountId, long podId) { + SearchCriteria sc = AccountPodSearch.create(); + sc.setParameters("account", accountId); + sc.setParameters("pod", podId); + sc.setParameters("destroyed", false); + sc.setParameters("status", AsyncInstanceCreateStatus.Created); + + return listBy(sc); + } + + @Override + public List findByTemplateAndZone(long templateId, long zoneId) { + SearchCriteria sc = TemplateZoneSearch.create(); + sc.setParameters("template", templateId); + sc.setParameters("zone", zoneId); + + return listBy(sc); + } + + @Override @DB + public List findVMInstancesByStorageHost(long hostId, Volume.MirrorState mirrState) { + + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + List result = new ArrayList(); + + try { + String sql = SELECT_VM_SQL; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, hostId); + pstmt.setString(2, mirrState.toString()); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + result.add(rs.getLong(1)); + } + return result; + } catch (SQLException e) { + throw new CloudRuntimeException("DB Exception on: " + SELECT_VM_SQL, e); + } catch (Throwable e) { + throw new CloudRuntimeException("Caught: " + SELECT_VM_SQL, e); + } + } + + @Override + public List findStrandedMirrorVolumes() { + SearchCriteria sc = MirrorSearch.create(); + sc.setParameters("mirrorState", MirrorState.ACTIVE.toString()); + + return listBy(sc); + } + + @Override + public boolean isAnyVolumeActivelyUsingTemplateOnPool(long templateId, long poolId) { + SearchCriteria sc = ActiveTemplateSearch.create(); + sc.setParameters("template", templateId); + sc.setParameters("pool", poolId); + + List results = searchAll(sc, null); + assert results.size() > 0 : "How can this return a size of " + results.size(); + + return results.get(0) > 0; + } + + @Override + public void deleteVolumesByInstance(long instanceId) { + SearchCriteria sc = InstanceIdSearch.create(); + sc.setParameters("instanceId", instanceId); + delete(sc); + } + + @Override + public void attachVolume(long volumeId, long vmId, long deviceId) { + VolumeVO volume = createForUpdate(volumeId); + volume.setInstanceId(vmId); + volume.setDeviceId(deviceId); + volume.setUpdated(new Date()); + update(volumeId, volume); + } + + @Override + public void detachVolume(long volumeId) { + VolumeVO volume = createForUpdate(volumeId); + volume.setInstanceId(null); + volume.setDeviceId(null); + volume.setUpdated(new Date()); + update(volumeId, volume); + } + + @Override + public void destroyVolume(long volumeId) { + VolumeVO volume = createForUpdate(volumeId); + volume.setDestroyed(true); + update(volumeId, volume); + } + + @Override + public void recoverVolume(long volumeId) { + VolumeVO volume = createForUpdate(volumeId); + volume.setDestroyed(false); + update(volumeId, volume); + } + + protected VolumeDaoImpl() { + AccountIdSearch = createSearchBuilder(); + AccountIdSearch.and("accountId", AccountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountIdSearch.and("destroyed", AccountIdSearch.entity().getDestroyed(), SearchCriteria.Op.EQ); + AccountIdSearch.done(); + + DetachedAccountIdSearch = createSearchBuilder(); + DetachedAccountIdSearch.and("accountId", DetachedAccountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + DetachedAccountIdSearch.and("destroyed", DetachedAccountIdSearch.entity().getDestroyed(), SearchCriteria.Op.EQ); + DetachedAccountIdSearch.and("instanceId", DetachedAccountIdSearch.entity().getInstanceId(), SearchCriteria.Op.NULL); + DetachedAccountIdSearch.done(); + + AccountPodSearch = createSearchBuilder(); + AccountPodSearch.and("account", AccountPodSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountPodSearch.and("pod", AccountPodSearch.entity().getPodId(), SearchCriteria.Op.EQ); + AccountPodSearch.and("destroyed", AccountPodSearch.entity().getDestroyed(), SearchCriteria.Op.EQ); + AccountPodSearch.and("status", AccountPodSearch.entity().getStatus(), SearchCriteria.Op.EQ); + AccountPodSearch.done(); + + TemplateZoneSearch = createSearchBuilder(); + TemplateZoneSearch.and("template", TemplateZoneSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateZoneSearch.and("zone", TemplateZoneSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + TemplateZoneSearch.done(); + + TotalSizeByPoolSearch = createSearchBuilder(SumCount.class); + TotalSizeByPoolSearch.select("sum", Func.SUM, TotalSizeByPoolSearch.entity().getSize()); + TotalSizeByPoolSearch.select("count", Func.COUNT, (Object[])null); + TotalSizeByPoolSearch.and("poolId", TotalSizeByPoolSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + TotalSizeByPoolSearch.and("removed", TotalSizeByPoolSearch.entity().getRemoved(), SearchCriteria.Op.NULL); + TotalSizeByPoolSearch.done(); + + + InstanceIdCreatedSearch = createSearchBuilder(); + InstanceIdCreatedSearch.and("instanceId", InstanceIdCreatedSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + InstanceIdCreatedSearch.and("status", InstanceIdCreatedSearch.entity().getStatus(), SearchCriteria.Op.EQ); + InstanceIdCreatedSearch.and("destroyed", InstanceIdCreatedSearch.entity().getDestroyed(), SearchCriteria.Op.EQ); + InstanceIdCreatedSearch.done(); + + InstanceIdSearch = createSearchBuilder(); + InstanceIdSearch.and("instanceId", InstanceIdSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + InstanceIdSearch.done(); + + PoolIdSearch = createSearchBuilder(); + PoolIdSearch.and("poolId", PoolIdSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + PoolIdSearch.done(); + + InstanceAndTypeSearch= createSearchBuilder(); + InstanceAndTypeSearch.and("instanceId", InstanceAndTypeSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + InstanceAndTypeSearch.and("vType", InstanceAndTypeSearch.entity().getVolumeType(), SearchCriteria.Op.EQ); + InstanceAndTypeSearch.done(); + + InstanceIdDestroyedSearch = createSearchBuilder(); + InstanceIdDestroyedSearch.and("instanceId", InstanceIdDestroyedSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + InstanceIdDestroyedSearch.and("destroyed", InstanceIdDestroyedSearch.entity().getDestroyed(), SearchCriteria.Op.EQ); + InstanceIdDestroyedSearch.done(); + + DetachedDestroyedSearch = createSearchBuilder(); + DetachedDestroyedSearch.and("instanceId", DetachedDestroyedSearch.entity().getInstanceId(), SearchCriteria.Op.NULL); + DetachedDestroyedSearch.and("destroyed", DetachedDestroyedSearch.entity().getDestroyed(), SearchCriteria.Op.EQ); + DetachedDestroyedSearch.done(); + + MirrorSearch = createSearchBuilder(); + MirrorSearch.and("mirrorVolume", MirrorSearch.entity().getMirrorVolume(), Op.NULL); + MirrorSearch.and("mirrorState", MirrorSearch.entity().getMirrorState(), Op.EQ); + MirrorSearch.done(); + + ActiveTemplateSearch = createSearchBuilder(Long.class); + ActiveTemplateSearch.and("pool", ActiveTemplateSearch.entity().getPoolId(), SearchCriteria.Op.EQ); + ActiveTemplateSearch.and("template", ActiveTemplateSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + ActiveTemplateSearch.and("removed", ActiveTemplateSearch.entity().getRemoved(), SearchCriteria.Op.NULL); + ActiveTemplateSearch.select(null, Func.COUNT, null); + ActiveTemplateSearch.done(); + + RemovedButNotDestroyedSearch = createSearchBuilder(); + RemovedButNotDestroyedSearch.and("destroyed", RemovedButNotDestroyedSearch.entity().getDestroyed(), SearchCriteria.Op.EQ); + RemovedButNotDestroyedSearch.and("removed", RemovedButNotDestroyedSearch.entity().getRemoved(), SearchCriteria.Op.NNULL); + RemovedButNotDestroyedSearch.done(); + } + + @Override @DB(txn=false) + public Pair getCountAndTotalByPool(long poolId) { + SearchCriteria sc = TotalSizeByPoolSearch.create(); + sc.setParameters("poolId", poolId); + List results = searchAll(sc, null); + SumCount sumCount = results.get(0); + return new Pair(sumCount.count, sumCount.sum); + } + + public static class SumCount { + public long sum; + public long count; + public SumCount() { + } + } +} diff --git a/core/src/com/cloud/storage/preallocatedlun/PreallocatedLunDetailVO.java b/core/src/com/cloud/storage/preallocatedlun/PreallocatedLunDetailVO.java new file mode 100644 index 00000000000..4db79daf27a --- /dev/null +++ b/core/src/com/cloud/storage/preallocatedlun/PreallocatedLunDetailVO.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.preallocatedlun; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="ext_lun_details") +public class PreallocatedLunDetailVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private long id; + + @Column(name="ext_lun_id") + private long lunId; + + @Column(name="tag") + private String tag; + + protected PreallocatedLunDetailVO() { + } + + public long getId() { + return id; + } + + public long getLunId() { + return lunId; + } + + public String getTag() { + return tag; + } + + public PreallocatedLunDetailVO(long lunId, String tag) { + this.lunId = lunId; + this.tag = tag; + } + +} diff --git a/core/src/com/cloud/storage/preallocatedlun/PreallocatedLunVO.java b/core/src/com/cloud/storage/preallocatedlun/PreallocatedLunVO.java new file mode 100644 index 00000000000..ee61479e91f --- /dev/null +++ b/core/src/com/cloud/storage/preallocatedlun/PreallocatedLunVO.java @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.preallocatedlun; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name="ext_lun_alloc") +public class PreallocatedLunVO { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private long id; + + private String portal; + + @Column(name="target_iqn") + private String targetIqn; + + private int lun; + + @Column(name="data_center_id") + private long dataCenterId; + + private long size; + + @Column(name="taken", nullable=true) + @Temporal(value=TemporalType.TIMESTAMP) + private Date taken; + + @Column(name="volume_id") + private Long volumeId; + + public PreallocatedLunVO(long dataCenterId, String portal, String targetIqn, int lun, long size) { + this.portal = portal; + this.targetIqn = targetIqn; + this.lun = lun; + this.size = size; + this.taken = null; + this.volumeId = null; + this.dataCenterId = dataCenterId; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public long getId() { + return id; + } + + public Date getTaken() { + return taken; + } + + public Long getVolumeId() { + return volumeId; + } + + public void setId(long id) { + this.id = id; + } + + public String getPortal() { + return portal; + } + + public void setPortal(String portal) { + this.portal = portal; + } + + public String getTargetIqn() { + return targetIqn; + } + + public void setTargetIqn(String targetIqn) { + this.targetIqn = targetIqn; + } + + public int getLun() { + return lun; + } + + public void setLun(int lun) { + this.lun = lun; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public void setTaken(Date taken) { + this.taken = taken; + } + + public void setVolumeId(Long instanceId) { + this.volumeId = instanceId; + } + + protected PreallocatedLunVO() { + } +} diff --git a/core/src/com/cloud/storage/resource/DummySecondaryStorageResource.java b/core/src/com/cloud/storage/resource/DummySecondaryStorageResource.java new file mode 100644 index 00000000000..b15a538de78 --- /dev/null +++ b/core/src/com/cloud/storage/resource/DummySecondaryStorageResource.java @@ -0,0 +1,163 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.resource; + +import java.util.HashMap; +import java.util.Map; + +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckHealthAnswer; +import com.cloud.agent.api.CheckHealthCommand; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.GetStorageStatsAnswer; +import com.cloud.agent.api.GetStorageStatsCommand; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.PingStorageCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.agent.api.StartupStorageCommand; +import com.cloud.agent.api.storage.DownloadAnswer; +import com.cloud.agent.api.storage.DownloadCommand; +import com.cloud.agent.api.storage.DownloadProgressCommand; +import com.cloud.host.Host; +import com.cloud.host.Host.Type; +import com.cloud.resource.ServerResource; +import com.cloud.resource.ServerResourceBase; +import com.cloud.storage.Volume; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.template.TemplateInfo; + +public class DummySecondaryStorageResource extends ServerResourceBase implements ServerResource { + private static final Logger s_logger = Logger.getLogger(DummySecondaryStorageResource.class); + + String _dc; + String _pod; + String _guid; + String _dummyPath; + + private boolean _useServiceVm; + + public DummySecondaryStorageResource(boolean useServiceVM) { + setUseServiceVm(useServiceVM); + } + + @Override + protected String getDefaultScriptsDir() { + return "dummy"; + } + + @Override + public Answer executeRequest(Command cmd) { + if (cmd instanceof DownloadProgressCommand) { + return new DownloadAnswer(null, 100, cmd, + com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOADED, + "dummyFS", + "/dummy"); + } else if (cmd instanceof DownloadCommand) { + return new DownloadAnswer(null, 100, cmd, + com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOADED, + "dummyFS", + "/dummy"); + } else if (cmd instanceof GetStorageStatsCommand) { + return execute((GetStorageStatsCommand)cmd); + } else if (cmd instanceof CheckHealthCommand) { + return new CheckHealthAnswer((CheckHealthCommand)cmd, true); + } else { + return Answer.createUnsupportedCommandAnswer(cmd); + } + } + + @Override + public PingCommand getCurrentStatus(long id) { + return new PingStorageCommand(Host.Type.Storage, id, new HashMap()); + } + + @Override + public Type getType() { + return Host.Type.SecondaryStorage; + } + + @Override + public StartupCommand[] initialize() { + final StartupStorageCommand cmd = new StartupStorageCommand("dummy", + StoragePoolType.NetworkFilesystem, 1024*1024*1024*100L, + new HashMap()); + + cmd.setResourceType(Volume.StorageResourceType.SECONDARY_STORAGE); + cmd.setIqn(null); + cmd.setNfsShare(_guid); + + fillNetworkInformation(cmd); + cmd.setDataCenter(_dc); + cmd.setPod(_pod); + cmd.setGuid(_guid); + + cmd.setName(_guid); + cmd.setVersion(DummySecondaryStorageResource.class.getPackage().getImplementationVersion()); + /* gather TemplateInfo in second storage */ + final Map tInfo = new HashMap(); + tInfo.put("routing", TemplateInfo.getDefaultSystemVmTemplateInfo()); + cmd.setTemplateInfo(tInfo); + cmd.getHostDetails().put("mount.parent", "dummy"); + cmd.getHostDetails().put("mount.path", "dummy"); + cmd.getHostDetails().put("orig.url", _guid); + + String tok[] = _dummyPath.split(":"); + cmd.setPrivateIpAddress(tok[0]); + return new StartupCommand [] {cmd}; + } + + protected GetStorageStatsAnswer execute(GetStorageStatsCommand cmd) { + long size = 1024*1024*1024*100L; + return new GetStorageStatsAnswer(cmd, 0, size); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + + _guid = (String)params.get("guid"); + if (_guid == null) { + throw new ConfigurationException("Unable to find the guid"); + } + + _dc = (String)params.get("zone"); + if (_dc == null) { + throw new ConfigurationException("Unable to find the zone"); + } + _pod = (String)params.get("pod"); + + _dummyPath = (String)params.get("mount.path"); + if (_dummyPath == null) { + throw new ConfigurationException("Unable to find mount.path"); + } + return true; + } + + public void setUseServiceVm(boolean _useServiceVm) { + this._useServiceVm = _useServiceVm; + } + + public boolean useServiceVm() { + return _useServiceVm; + } +} diff --git a/core/src/com/cloud/storage/resource/NfsSecondaryStorageResource.java b/core/src/com/cloud/storage/resource/NfsSecondaryStorageResource.java new file mode 100644 index 00000000000..dc00d8983d0 --- /dev/null +++ b/core/src/com/cloud/storage/resource/NfsSecondaryStorageResource.java @@ -0,0 +1,676 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.resource; + +import java.io.File; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckHealthAnswer; +import com.cloud.agent.api.CheckHealthCommand; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.GetStorageStatsAnswer; +import com.cloud.agent.api.GetStorageStatsCommand; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.PingStorageCommand; +import com.cloud.agent.api.ReadyAnswer; +import com.cloud.agent.api.ReadyCommand; +import com.cloud.agent.api.SecStorageFirewallCfgCommand; +import com.cloud.agent.api.SecStorageSetupCommand; +import com.cloud.agent.api.StartupCommand; +import com.cloud.agent.api.StartupStorageCommand; +import com.cloud.agent.api.SecStorageFirewallCfgCommand.PortConfig; +import com.cloud.agent.api.storage.DeleteTemplateCommand; +import com.cloud.agent.api.storage.DownloadCommand; +import com.cloud.agent.api.storage.DownloadProgressCommand; +import com.cloud.host.Host; +import com.cloud.host.Host.Type; +import com.cloud.resource.ServerResource; +import com.cloud.resource.ServerResourceBase; +import com.cloud.storage.StorageLayer; +import com.cloud.storage.Volume; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.template.DownloadManager; +import com.cloud.storage.template.DownloadManagerImpl; +import com.cloud.storage.template.TemplateInfo; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; +import com.cloud.utils.net.NfsUtils; +import com.cloud.utils.script.Script; + +public class NfsSecondaryStorageResource extends ServerResourceBase implements ServerResource { + private static final Logger s_logger = Logger.getLogger(NfsSecondaryStorageResource.class); + int _timeout; + + String _instance; + String _parent; + + String _dc; + String _pod; + String _guid; + String _nfsPath; + String _mountParent; + Map _params; + StorageLayer _storage; + boolean _inSystemVM = false; + boolean _sslCopy = false; + + Random _rand = new Random(System.currentTimeMillis()); + + DownloadManager _dlMgr; + private String _configSslScr; + private String _configAuthScr; + private String _publicIp; + private String _hostname; + private String _localgw; + private String _eth1mask; + private String _eth1ip; + + @Override + public void disconnected() { + if (_parent != null && !_inSystemVM) { + Script script = new Script(!_inSystemVM, "umount", _timeout, s_logger); + script.add(_parent); + script.execute(); + + File file = new File(_parent); + file.delete(); + } + } + + @Override + public Answer executeRequest(Command cmd) { + if (cmd instanceof DownloadProgressCommand) { + return _dlMgr.handleDownloadCommand((DownloadProgressCommand)cmd); + } else if (cmd instanceof DownloadCommand) { + return _dlMgr.handleDownloadCommand((DownloadCommand)cmd); + } else if (cmd instanceof GetStorageStatsCommand) { + return execute((GetStorageStatsCommand)cmd); + } else if (cmd instanceof CheckHealthCommand) { + return new CheckHealthAnswer((CheckHealthCommand)cmd, true); + } else if (cmd instanceof DeleteTemplateCommand) { + return execute((DeleteTemplateCommand) cmd); + } else if (cmd instanceof ReadyCommand) { + return new ReadyAnswer((ReadyCommand)cmd); + } else if (cmd instanceof SecStorageFirewallCfgCommand){ + return execute((SecStorageFirewallCfgCommand)cmd); + } else if (cmd instanceof SecStorageSetupCommand){ + return execute((SecStorageSetupCommand)cmd); + } else { + return Answer.createUnsupportedCommandAnswer(cmd); + } + } + + private Answer execute(SecStorageSetupCommand cmd) { + if (!_inSystemVM){ + return new Answer(cmd, true, null); + } + boolean success = true; + StringBuilder result = new StringBuilder(); + for (String cidr: cmd.getAllowedInternalSites()) { + String tmpresult = allowOutgoingOnPrivate(cidr); + if (tmpresult != null) { + result.append(", ").append(tmpresult); + success = false; + } + } + if (success) { + if (cmd.getCopyPassword() != null && cmd.getCopyUserName() != null) { + String tmpresult = configureAuth(cmd.getCopyUserName(), cmd.getCopyPassword()); + if (tmpresult != null) { + result.append("Failed to configure auth for copy ").append(tmpresult); + success = false; + } + } + } + return new Answer(cmd, success, result.toString()); + + } + + private String allowOutgoingOnPrivate(String destCidr) { + + Script command = new Script("/bin/bash", s_logger); + String intf = "eth1"; + command.add("-c"); + command.add("iptables -I OUTPUT -o " + intf + " -d " + destCidr + " -p tcp -m state --state NEW -m tcp -j ACCEPT"); + + String result = command.execute(); + if (result != null) { + s_logger.warn("Error in allowing outgoing to " + destCidr + ", err=" + result ); + return "Error in allowing outgoing to " + destCidr + ", err=" + result; + } + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, destCidr); + return null; + } + + + + private Answer execute(SecStorageFirewallCfgCommand cmd) { + if (!_inSystemVM){ + return new Answer(cmd, true, null); + } + List iptablesCfg = new ArrayList(); + iptablesCfg.add("iptables -F HTTP"); + for (PortConfig pCfg:cmd.getPortConfigs()){ + if (pCfg.isAdd()) { + iptablesCfg.add("iptables -A HTTP -i " + pCfg.getIntf() + " -s " + pCfg.getSourceIp() + " -p tcp -m state --state NEW -m tcp --dport " + pCfg.getPort() + " -j ACCEPT"); + } + } + boolean success = true; + StringBuilder result = new StringBuilder(); + for (String rule: iptablesCfg) { + Script command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add(rule); + String tmpresult = command.execute(); + if (tmpresult != null) { + result.append(tmpresult); + success = false; + } + } + return new Answer(cmd, success, result.toString()); + } + + protected GetStorageStatsAnswer execute(final GetStorageStatsCommand cmd) { + final long usedSize = getUsedSize(); + final long totalSize = getTotalSize(); + if (usedSize == -1 || totalSize == -1) { + return new GetStorageStatsAnswer(cmd, "Unable to get storage stats"); + } else { + return new GetStorageStatsAnswer(cmd, totalSize, usedSize) ; + } + } + + protected Answer execute(final DeleteTemplateCommand cmd) { + String relativeTemplatePath = cmd.getTemplatePath(); + String parent = _parent; + + if (relativeTemplatePath.startsWith(File.separator)) { + relativeTemplatePath = relativeTemplatePath.substring(1); + } + + if (!parent.endsWith(File.separator)) { + parent += File.separator; + } + String absoluteTemplatePath = parent + relativeTemplatePath; + File tmpltParent = new File(absoluteTemplatePath).getParentFile(); + + boolean result = true; + if (tmpltParent.exists()) { + File [] tmpltFiles = tmpltParent.listFiles(); + if (tmpltFiles != null) { + for (File f : tmpltFiles) { + f.delete(); + } + } + + result = _storage.delete(tmpltParent.getAbsolutePath()); + } + + if (result) { + return new Answer(cmd, true, null); + } else { + return new Answer(cmd, false, "Failed to delete file"); + } + } + + protected long getUsedSize() { + return _storage.getUsedSpace(_parent); + } + + protected long getTotalSize() { + return _storage.getTotalSpace(_parent); + } + + protected long convertFilesystemSize(final String size) { + if (size == null || size.isEmpty()) { + return -1; + } + + long multiplier = 1; + if (size.endsWith("T")) { + multiplier = 1024l * 1024l * 1024l * 1024l; + } else if (size.endsWith("G")) { + multiplier = 1024l * 1024l * 1024l; + } else if (size.endsWith("M")) { + multiplier = 1024l * 1024l; + } else { + assert (false) : "Well, I have no idea what this is: " + size; + } + + return (long)(Double.parseDouble(size.substring(0, size.length() - 1)) * multiplier); + } + + + @Override + public Type getType() { + return Host.Type.SecondaryStorage; + } + + @Override + public PingCommand getCurrentStatus(final long id) { + return new PingStorageCommand(Host.Type.Storage, id, new HashMap()); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + _eth1ip = (String)params.get("eth1ip"); + if (_eth1ip != null) { //can only happen inside service vm + params.put("private.network.device", "eth1"); + } else { + s_logger.warn("Wait, what's going on? eth1ip is null!!"); + } + String eth2ip = (String) params.get("eth2ip"); + if (eth2ip != null) { + params.put("public.network.device", "eth2"); + } + _publicIp = (String) params.get("eth2ip"); + _hostname = (String) params.get("name"); + + super.configure(name, params); + + _params = params; + String value = (String)params.get("scripts.timeout"); + _timeout = NumbersUtil.parseInt(value, 1440) * 1000; + + _storage = (StorageLayer)params.get(StorageLayer.InstanceConfigKey); + if (_storage == null) { + value = (String)params.get(StorageLayer.ClassConfigKey); + if (value == null) { + value = "com.cloud.storage.JavaStorageLayer"; + } + + try { + Class clazz = Class.forName(value); + _storage = (StorageLayer)ComponentLocator.inject(clazz); + _storage.configure("StorageLayer", params); + } catch (ClassNotFoundException e) { + throw new ConfigurationException("Unable to find class " + value); + } + } + _configSslScr = Script.findScript(getDefaultScriptsDir(), "config_ssl.sh"); + if (_configSslScr != null) { + s_logger.info("config_ssl.sh found in " + _configSslScr); + } + + _configAuthScr = Script.findScript(getDefaultScriptsDir(), "config_auth.sh"); + if (_configSslScr != null) { + s_logger.info("config_auth.sh found in " + _configAuthScr); + } + + _guid = (String)params.get("guid"); + if (_guid == null) { + throw new ConfigurationException("Unable to find the guid"); + } + + _dc = (String)params.get("zone"); + if (_dc == null) { + throw new ConfigurationException("Unable to find the zone"); + } + _pod = (String)params.get("pod"); + + _instance = (String)params.get("instance"); + + _mountParent = (String)params.get("mount.parent"); + if (_mountParent == null) { + _mountParent = File.separator + "mnt"; + } + + if (_instance != null) { + _mountParent = _mountParent + File.separator + _instance; + } + + _nfsPath = (String)params.get("mount.path"); + if (_nfsPath == null) { + throw new ConfigurationException("Unable to find mount.path"); + } + + + + String inSystemVM = (String)params.get("secondary.storage.vm"); + if (inSystemVM == null || "true".equalsIgnoreCase(inSystemVM)) { + _inSystemVM = true; + _localgw = (String)params.get("localgw"); + if (_localgw != null) { //can only happen inside service vm + _eth1mask = (String)params.get("eth1mask"); + String internalDns1 = (String)params.get("dns1"); + String internalDns2 = (String)params.get("dns2"); + + if (internalDns1 == null) { + s_logger.warn("No DNS entry found during configuration of NfsSecondaryStorage"); + } else { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, internalDns1); + } + + String mgmtHost = (String)params.get("host"); + String nfsHost = NfsUtils.getHostPart(_nfsPath); + if (nfsHost == null) { + s_logger.error("Invalid or corrupt nfs url " + _nfsPath); + throw new CloudRuntimeException("Unable to determine host part of nfs path"); + } + try { + InetAddress nfsHostAddr = InetAddress.getByName(nfsHost); + nfsHost = nfsHostAddr.getHostAddress(); + } catch (UnknownHostException uhe) { + s_logger.error("Unable to resolve nfs host " + nfsHost); + throw new CloudRuntimeException("Unable to resolve nfs host to an ip address " + nfsHost); + } + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, nfsHost); + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + if (internalDns2 != null) { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, internalDns2); + } + + } + String useSsl = (String)params.get("sslcopy"); + if (useSsl != null) { + _sslCopy = Boolean.parseBoolean(useSsl); + if (_sslCopy) { + configureSSL(); + } + } + startAdditionalServices(); + _params.put("install.numthreads", "50"); + _params.put("secondary.storage.vm", "true"); + } + _parent = mount(_nfsPath, _mountParent); + if (_parent == null) { + throw new ConfigurationException("Unable to create mount point"); + } + + + s_logger.info("Mount point established at " + _parent); + + try { + _params.put("template.parent", _parent); + _params.put(StorageLayer.InstanceConfigKey, _storage); + _dlMgr = new DownloadManagerImpl(); + _dlMgr.configure("DownloadManager", _params); + } catch (ConfigurationException e) { + s_logger.warn("Caught problem while configuring DownloadManager", e); + return false; + } + return true; + } + + private void startAdditionalServices() { + Script command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add("service sshd restart "); + String result = command.execute(); + if (result != null) { + s_logger.warn("Error in starting sshd service err=" + result ); + } + command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add("iptables -I INPUT -i eth1 -p tcp -m state --state NEW -m tcp --dport 3922 -j ACCEPT"); + result = command.execute(); + if (result != null) { + s_logger.warn("Error in opening up ssh port err=" + result ); + } + } + + private void addRouteToInternalIpOrCidr(String localgw, String eth1ip, String eth1mask, String destIpOrCidr) { + s_logger.debug("addRouteToInternalIp: localgw=" + localgw + ", eth1ip=" + eth1ip + ", eth1mask=" + eth1mask + ",destIp=" + destIpOrCidr); + if (destIpOrCidr == null) { + s_logger.debug("addRouteToInternalIp: destIp is null"); + return; + } + if (!NetUtils.isValidIp(destIpOrCidr) && !NetUtils.isValidCIDR(destIpOrCidr)){ + s_logger.warn(" destIp is not a valid ip address or cidr destIp=" + destIpOrCidr); + return; + } + boolean inSameSubnet = false; + if (NetUtils.isValidIp(destIpOrCidr)) { + if (eth1ip != null && eth1mask != null) { + inSameSubnet = NetUtils.sameSubnet(eth1ip, destIpOrCidr, eth1mask); + } else { + s_logger.warn("addRouteToInternalIp: unable to determine same subnet: _eth1ip=" + eth1ip + ", dest ip=" + destIpOrCidr + ", _eth1mask=" + eth1mask); + } + } else { + inSameSubnet = NetUtils.isNetworkAWithinNetworkB(destIpOrCidr, NetUtils.ipAndNetMaskToCidr(eth1ip, eth1mask)); + } + if (inSameSubnet) { + s_logger.debug("addRouteToInternalIp: dest ip " + destIpOrCidr + " is in the same subnet as eth1 ip " + eth1ip); + return; + } + Script command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add("ip route delete " + destIpOrCidr); + command.execute(); + command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add("ip route add " + destIpOrCidr + " via " + localgw); + String result = command.execute(); + if (result != null) { + s_logger.warn("Error in configuring route to internal ip err=" + result ); + } else { + s_logger.debug("addRouteToInternalIp: added route to internal ip=" + destIpOrCidr + " via " + localgw); + } + } + + private void configureSSL() { + Script command = new Script(_configSslScr); + command.add(_publicIp); + command.add(_hostname); + String result = command.execute(); + if (result != null) { + s_logger.warn("Unable to configure httpd to use ssl"); + } + } + + private String configureAuth(String user, String passwd) { + Script command = new Script(_configAuthScr); + command.add(user); + command.add(passwd); + String result = command.execute(); + if (result != null) { + s_logger.warn("Unable to configure httpd to use auth"); + } + return result; + } + + protected String mount(String path, String parent) { + String mountPoint = null; + for (int i = 0; i < 10; i++) { + String mntPt = parent + File.separator + Integer.toHexString(_rand.nextInt(Integer.MAX_VALUE)); + File file = new File(mntPt); + if (!file.exists()) { + if (_storage.mkdir(mntPt)) { + mountPoint = mntPt; + break; + } + } + s_logger.debug("Unable to create mount: " + mntPt); + } + + if (mountPoint == null) { + s_logger.warn("Unable to create a mount point"); + return null; + } + + Script script = null; + String result = null; + script = new Script(!_inSystemVM, "umount", _timeout, s_logger); + script.add(path); + result = script.execute(); + + if( _parent != null ) { + script = new Script("rmdir", _timeout, s_logger); + script.add(_parent); + result = script.execute(); + } + + Script command = new Script(!_inSystemVM, "mount", _timeout, s_logger); + command.add("-t", "nfs"); + if (_inSystemVM) { + //Fedora Core 12 errors out with any -o option executed from java + command.add("-o", "soft,timeo=133,retrans=2147483647,tcp,acdirmax=0,acdirmin=0"); + } + command.add(path); + command.add(mountPoint); + result = command.execute(); + if (result != null) { + s_logger.warn("Unable to mount " + path + " due to " + result); + File file = new File(mountPoint); + if (file.exists()) + file.delete(); + return null; + } + + // Change permissions for the mountpoint + script = new Script(!_inSystemVM, "chmod", _timeout, s_logger); + script.add("777", mountPoint); + result = script.execute(); + if (result != null) { + s_logger.warn("Unable to set permissions for " + mountPoint + " due to " + result); + return null; + } + + // XXX: Adding the check for creation of snapshots dir here. Might have to move it somewhere more logical later. + if (!checkForSnapshotsDir(mountPoint)) { + return null; + } + + // Create the volumes dir + if (!checkForVolumesDir(mountPoint)) { + return null; + } + + return mountPoint; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } + + @Override + public StartupCommand[] initialize() { + /*disconnected(); + + _parent = mount(_nfsPath, _mountParent); + + if( _parent == null ) { + s_logger.warn("Unable to mount the nfs server"); + return null; + } + + try { + _params.put("template.parent", _parent); + _params.put(StorageLayer.InstanceConfigKey, _storage); + _dlMgr = new DownloadManagerImpl(); + _dlMgr.configure("DownloadManager", _params); + } catch (ConfigurationException e) { + s_logger.warn("Caught problem while configuring folers", e); + return null; + }*/ + + final StartupStorageCommand cmd = new StartupStorageCommand(_parent, StoragePoolType.NetworkFilesystem, getTotalSize(), new HashMap()); + + cmd.setResourceType(Volume.StorageResourceType.SECONDARY_STORAGE); + cmd.setIqn(null); + + fillNetworkInformation(cmd); + cmd.setDataCenter(_dc); + cmd.setPod(_pod); + cmd.setGuid(_guid); + cmd.setName(_guid); + cmd.setVersion(NfsSecondaryStorageResource.class.getPackage().getImplementationVersion()); + /* gather TemplateInfo in second storage */ + final Map tInfo = _dlMgr.gatherTemplateInfo(); + cmd.setTemplateInfo(tInfo); + cmd.getHostDetails().put("mount.parent", _mountParent); + cmd.getHostDetails().put("mount.path", _nfsPath); + String tok[] = _nfsPath.split(":"); + cmd.setNfsShare("nfs://" + tok[0] + tok[1]); + if (cmd.getHostDetails().get("orig.url") == null) { + if (tok.length != 2) { + throw new CloudRuntimeException("Not valid NFS path" + _nfsPath); + } + String nfsUrl = "nfs://" + tok[0] + tok[1]; + cmd.getHostDetails().put("orig.url", nfsUrl); + } + InetAddress addr; + try { + addr = InetAddress.getByName(tok[0]); + cmd.setPrivateIpAddress(addr.getHostAddress()); + } catch (UnknownHostException e) { + cmd.setPrivateIpAddress(tok[0]); + } + return new StartupCommand [] {cmd}; + } + + protected boolean checkForSnapshotsDir(String mountPoint) { + String snapshotsDirLocation = mountPoint + File.separator + "snapshots"; + return createDir("snapshots", snapshotsDirLocation, mountPoint); + } + + protected boolean checkForVolumesDir(String mountPoint) { + String volumesDirLocation = mountPoint + "/" + "volumes"; + return createDir("volumes", volumesDirLocation, mountPoint); + } + + protected boolean createDir(String dirName, String dirLocation, String mountPoint) { + boolean dirExists = false; + + File dir = new File(dirLocation); + if (dir.exists()) { + if (dir.isDirectory()) { + s_logger.debug(dirName + " already exists on secondary storage, and is mounted at " + mountPoint); + dirExists = true; + } else { + if (dir.delete() && _storage.mkdir(dirLocation)) { + dirExists = true; + } + } + } else if (_storage.mkdir(dirLocation)) { + dirExists = true; + } + + if (dirExists) { + s_logger.info(dirName + " directory created/exists on Secondary Storage."); + } else { + s_logger.info(dirName + " directory does not exist on Secondary Storage."); + } + + return dirExists; + } + + @Override + protected String getDefaultScriptsDir() { + return "./scripts/storage/secondary"; + } +} diff --git a/core/src/com/cloud/storage/resource/StoragePoolResource.java b/core/src/com/cloud/storage/resource/StoragePoolResource.java new file mode 100644 index 00000000000..4207f8b6273 --- /dev/null +++ b/core/src/com/cloud/storage/resource/StoragePoolResource.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.resource; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.storage.CopyVolumeAnswer; +import com.cloud.agent.api.storage.CopyVolumeCommand; +import com.cloud.agent.api.storage.CreateAnswer; +import com.cloud.agent.api.storage.CreateCommand; +import com.cloud.agent.api.storage.DestroyCommand; +import com.cloud.agent.api.storage.DownloadAnswer; +import com.cloud.agent.api.storage.PrimaryStorageDownloadCommand; +import com.cloud.agent.api.storage.ShareAnswer; +import com.cloud.agent.api.storage.ShareCommand; + +public interface StoragePoolResource { + // FIXME: Should have a PrimaryStorageDownloadAnswer + DownloadAnswer execute(PrimaryStorageDownloadCommand cmd); + + // FIXME: Should have an DestroyAnswer + Answer execute(DestroyCommand cmd); + + ShareAnswer execute(ShareCommand cmd); + + CopyVolumeAnswer execute(CopyVolumeCommand cmd); + + CreateAnswer execute(CreateCommand cmd); +} diff --git a/core/src/com/cloud/storage/secondary/SecStorageVmAlertEventArgs.java b/core/src/com/cloud/storage/secondary/SecStorageVmAlertEventArgs.java new file mode 100644 index 00000000000..bf770dd61fe --- /dev/null +++ b/core/src/com/cloud/storage/secondary/SecStorageVmAlertEventArgs.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.secondary; + +import com.cloud.utils.events.EventArgs; +import com.cloud.vm.SecondaryStorageVmVO; + +public class SecStorageVmAlertEventArgs extends EventArgs { + + private static final long serialVersionUID = 23773987551479885L; + + public static final int SSVM_CREATED = 1; + public static final int SSVM_UP = 2; + public static final int SSVM_DOWN = 3; + public static final int SSVM_CREATE_FAILURE = 4; + public static final int SSVM_START_FAILURE = 5; + public static final int SSVM_FIREWALL_ALERT = 6; + public static final int SSVM_STORAGE_ALERT = 7; + public static final int SSVM_REBOOTED = 8; + + public static final String ALERT_SUBJECT = "ssvm-alert"; + + + private int type; + private long zoneId; + private long ssVmId; + private SecondaryStorageVmVO ssVm; + private String message; + + public SecStorageVmAlertEventArgs(int type, long zoneId, + long ssVmId, SecondaryStorageVmVO ssVm, String message) { + + super(ALERT_SUBJECT); + this.type = type; + this.zoneId = zoneId; + this.ssVmId = ssVmId; + this.ssVm = ssVm; + this.message = message; + } + + public int getType() { + return type; + } + + public long getZoneId() { + return zoneId; + } + + public long getSecStorageVmId() { + return ssVmId; + } + + public SecondaryStorageVmVO getSecStorageVm() { + return ssVm; + } + + public String getMessage() { + return message; + } +} diff --git a/core/src/com/cloud/storage/secondary/SecondaryStorageVmAllocator.java b/core/src/com/cloud/storage/secondary/SecondaryStorageVmAllocator.java new file mode 100644 index 00000000000..5dc8d8731cb --- /dev/null +++ b/core/src/com/cloud/storage/secondary/SecondaryStorageVmAllocator.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.secondary; + +import java.util.List; +import java.util.Map; + +import com.cloud.utils.component.Adapter; +import com.cloud.vm.SecondaryStorageVmVO; + +public interface SecondaryStorageVmAllocator extends Adapter { + public SecondaryStorageVmVO allocSecondaryStorageVm(List candidates, Map loadInfo, long dataCenterId); +} diff --git a/core/src/com/cloud/storage/secondary/SecondaryStorageVmManager.java b/core/src/com/cloud/storage/secondary/SecondaryStorageVmManager.java new file mode 100644 index 00000000000..36d677b6276 --- /dev/null +++ b/core/src/com/cloud/storage/secondary/SecondaryStorageVmManager.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.secondary; + +import com.cloud.agent.api.StartupCommand; +import com.cloud.host.HostVO; +import com.cloud.utils.component.Manager; +import com.cloud.vm.SecondaryStorageVmVO; + +public interface SecondaryStorageVmManager extends Manager { + + public static final int DEFAULT_SS_VM_RAMSIZE = 256; // 256M + + + public static final String ALERT_SUBJECT = "secondarystoragevm-alert"; + + public SecondaryStorageVmVO startSecStorageVm(long ssVmVmId, long startEventId); + public boolean stopSecStorageVm(long ssVmVmId, long startEventId); + public boolean rebootSecStorageVm(long ssVmVmId, long startEventId); + public boolean destroySecStorageVm(long ssVmVmId, long startEventId); + public void onAgentConnect(Long dcId, StartupCommand cmd); + public boolean generateFirewallConfiguration(Long agentId); + public boolean generateSetupCommand(Long zoneId); + +} diff --git a/core/src/com/cloud/storage/template/DownloadManager.java b/core/src/com/cloud/storage/template/DownloadManager.java new file mode 100644 index 00000000000..aab5bc882fb --- /dev/null +++ b/core/src/com/cloud/storage/template/DownloadManager.java @@ -0,0 +1,112 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +import java.util.List; +import java.util.Map; + +import com.cloud.agent.api.storage.DownloadAnswer; +import com.cloud.agent.api.storage.DownloadCommand; +import com.cloud.storage.StorageResource; +import com.cloud.storage.VMTemplateHostVO; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.utils.component.Manager; + +/** + * @author chiradeep + * + */ +public interface DownloadManager extends Manager { + + /** + * Initiate download of a public template + * @param id unique id. + * @param url the url from where to download from + * @param name the user-supplied name for the template + * @param format the file format of the template + * @param hvm whether the template is a hardware virtual machine + * @param accountId the accountId of the iso owner (null if public iso) + * @param descr description of the template + * @param cksum checksum for the downloaded file (note: not of the final installed template) + * @param the name if the ISO file if the template being downloaded is an ISO + * @param user username used for authentication to the server + * @param password password used for authentication to the server + * @param maxDownloadSizeInBytes (optional) max download size for the template, in bytes. + * @return job-id that can be used to interrogate the status of the download. + */ + public String downloadPublicTemplate(long id, String url, String name, ImageFormat format, boolean hvm, Long accountId, String descr, String cksum, String installPathPrefix, String userName, String passwd, long maxDownloadSizeInBytes); + + + /** + * Get the status of a download job + * @param jobId job Id + * @return status of the download job + */ + public TemplateDownloader.Status getDownloadStatus(String jobId); + + /** + * Get the status of a download job + * @param jobId job Id + * @return status of the download job + */ + public VMTemplateHostVO.Status getDownloadStatus2(String jobId); + + /** + * Get the download percent of a download job + * @param jobId job Id + * @return + */ + public int getDownloadPct(String jobId); + + /** + * Get the download error if any + * @param jobId job Id + * @return + */ + public String getDownloadError(String jobId); + + /** + * Get the local path for the download + * @param jobId job Id + * @return + public String getDownloadLocalPath(String jobId); + */ + + /** Handle download commands from the management server + * @param cmd cmd from server + * @return answer representing status of download. + */ + public DownloadAnswer handleDownloadCommand(DownloadCommand cmd); + + /** + * List the paths of the public templates successfully installed in the public templates location + * @return + */ + public List listPublicTemplates(); + + /** + /** + * @return list of template info for installed templates + */ + public Map gatherTemplateInfo(); + + public String setRootDir(String rootDir, StorageResource storage); + + public String getPublicTemplateRepo(); + +} \ No newline at end of file diff --git a/core/src/com/cloud/storage/template/DownloadManagerImpl.java b/core/src/com/cloud/storage/template/DownloadManagerImpl.java new file mode 100644 index 00000000000..2ec957a6d8a --- /dev/null +++ b/core/src/com/cloud/storage/template/DownloadManagerImpl.java @@ -0,0 +1,904 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.agent.api.storage.DownloadAnswer; +import com.cloud.agent.api.storage.DownloadCommand; +import com.cloud.agent.api.storage.DownloadProgressCommand; +import com.cloud.agent.api.storage.DownloadProgressCommand.RequestType; +import com.cloud.exception.InternalErrorException; +import com.cloud.storage.StorageLayer; +import com.cloud.storage.StorageResource; +import com.cloud.storage.VMTemplateHostVO; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.template.Processor.FormatInfo; +import com.cloud.storage.template.TemplateDownloader.DownloadCompleteCallback; +import com.cloud.storage.template.TemplateDownloader.Status; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.UUID; +import com.cloud.utils.component.Adapters; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; + +/** + * @author chiradeep + * + */ +@Local(value = DownloadManager.class) +public class DownloadManagerImpl implements DownloadManager { + private String _name; + StorageLayer _storage; + Adapters _processors; + + public class Completion implements DownloadCompleteCallback { + private final String jobId; + + public Completion(String jobId) { + this.jobId = jobId; + } + + @Override + public void downloadComplete(Status status) { + setDownloadStatus(jobId, status); + } + } + + private static class DownloadJob { + private final TemplateDownloader td; + private final String jobId; + private final String tmpltName; + private final boolean hvm; + private final ImageFormat format; + private String tmpltPath; + private String description; + private String checksum; + private Long accountId; + private String installPathPrefix; + private long templatesize; + private long id; + + public DownloadJob(TemplateDownloader td, String jobId, long id, String tmpltName, ImageFormat format, boolean hvm, Long accountId, String descr, String cksum, String installPathPrefix) { + super(); + this.td = td; + this.jobId = jobId; + this.tmpltName = tmpltName; + this.format = format; + this.hvm = hvm; + this.accountId = accountId; + this.description = descr; + this.checksum = cksum; + this.installPathPrefix = installPathPrefix; + this.templatesize = 0; + this.id = id; + } + + public TemplateDownloader getTd() { + return td; + } + + public String getDescription() { + return description; + } + + public String getChecksum() { + return checksum; + } + + public DownloadJob(TemplateDownloader td, String jobId, DownloadCommand cmd) { + this.td = td; + this.jobId = jobId; + this.tmpltName = cmd.getName(); + this.format = cmd.getFormat(); + this.hvm = cmd.isHvm(); + } + + public TemplateDownloader getTemplateDownloader() { + return td; + } + + public String getJobId() { + return jobId; + } + + public String getTmpltName() { + return tmpltName; + } + + public ImageFormat getFormat() { + return format; + } + + public boolean isHvm() { + return hvm; + } + + public Long getAccountId() { + return accountId; + } + + public long getId() { + return id; + } + + public void setTmpltPath(String tmpltPath) { + this.tmpltPath = tmpltPath; + } + + public String getTmpltPath() { + return tmpltPath; + } + + public String getInstallPathPrefix() { + return installPathPrefix; + } + + public void cleanup() { + if (td != null) { + String dnldPath = td.getDownloadLocalPath(); + if (dnldPath != null) { + File f = new File(dnldPath); + File dir = f.getParentFile(); + f.delete(); + if (dir != null) + dir.delete(); + } + } + + } + + public void setTemplatesize(long templatesize) { + this.templatesize = templatesize; + } + + public long getTemplatesize() { + return templatesize; + } + } + + public static final Logger s_logger = Logger.getLogger(DownloadManagerImpl.class); + private String parentDir; + private String publicTemplateRepo; + private String createTmpltScr; + private Adapters processors; + + private ExecutorService threadPool; + + private final Map jobs = new ConcurrentHashMap(); + private String listTmpltScr; + private int installTimeoutPerGig = 180 * 60 * 1000; + private boolean _sslCopy; + + public String setRootDir(String rootDir, StorageResource storage) { + /* + * if (!storage.existPath(rootDir + templateDownloadDir)) { s_logger.info("Creating template download path: " + + * rootDir + templateDownloadDir); String result = storage.createPath(rootDir + templateDownloadDir); if (result + * != null) { return "Cannot create " + rootDir + templateDownloadDir + " due to " + result; } } + * this.templateDownloadDir = rootDir + templateDownloadDir; + */ + this.publicTemplateRepo = rootDir + publicTemplateRepo; + + return null; + } + + /** + * Get notified of change of job status. Executed in context of downloader thread + * + * @param jobId + * the id of the job + * @param status + * the status of the job + */ + public void setDownloadStatus(String jobId, Status status) { + DownloadJob dj = jobs.get(jobId); + if (dj == null) { + s_logger.warn("setDownloadStatus for jobId: " + jobId + ", status=" + status + " no job found"); + return; + } + TemplateDownloader td = dj.getTemplateDownloader(); + s_logger.info("Download Completion for jobId: " + jobId + ", status=" + status); + s_logger.info("local: " + td.getDownloadLocalPath() + ", bytes=" + td.getDownloadedBytes() + ", error=" + td.getDownloadError() + ", pct=" + td.getDownloadPercent()); + + switch (status) { + case ABORTED: + case NOT_STARTED: + case UNRECOVERABLE_ERROR: + // TODO + dj.cleanup(); + break; + case UNKNOWN: + return; + case IN_PROGRESS: + s_logger.info("Resuming jobId: " + jobId + ", status=" + status); + td.setResume(true); + threadPool.execute(td); + break; + case RECOVERABLE_ERROR: + threadPool.execute(td); + break; + case DOWNLOAD_FINISHED: + td.setDownloadError("Download success, starting install "); + String result = postDownload(jobId); + if (result != null) { + s_logger.error("Failed post download script: " + result); + td.setStatus(Status.UNRECOVERABLE_ERROR); + td.setDownloadError("Failed post download script: " + result); + } else { + td.setStatus(Status.POST_DOWNLOAD_FINISHED); + td.setDownloadError("Install completed successfully at " + new SimpleDateFormat().format(new Date())); + } + dj.cleanup(); + break; + default: + break; + } + } + + /** + * Post download activity (install and cleanup). Executed in context of downloader thread + * + * @throws IOException + */ + private String postDownload(String jobId) { + DownloadJob dnld = jobs.get(jobId); + TemplateDownloader td = dnld.getTemplateDownloader(); + String templatePath = null; + templatePath = dnld.getInstallPathPrefix() + dnld.getAccountId() + File.separator + dnld.getId() + File.separator;// dnld.getTmpltName(); + + _storage.mkdirs(templatePath); + + // once template path is set, remove the parent dir so that the template is installed with a relative path + String finalTemplatePath = templatePath.substring(parentDir.length()); + dnld.setTmpltPath(finalTemplatePath); + + int imgSizeGigs = (int) Math.ceil(_storage.getSize(td.getDownloadLocalPath()) * 1.0d / (1024 * 1024 * 1024)); + imgSizeGigs++; // add one just in case + long timeout = imgSizeGigs * installTimeoutPerGig; + Script scr = null; + scr = new Script(createTmpltScr, timeout, s_logger); + scr.add("-s", Integer.toString(imgSizeGigs)); + scr.add("-S", Long.toString(td.getMaxTemplateSizeInBytes())); + if (dnld.getDescription() != null && dnld.getDescription().length() > 1) { + scr.add("-d", dnld.getDescription()); + } + if (dnld.isHvm()) { + scr.add("-h"); + } + + // add options common to ISO and template + String extension = dnld.getFormat().toString().toLowerCase(); + String templateName = ""; + if( extension.equals("iso")) { + templateName = jobs.get(jobId).getTmpltName().trim().replace(" ", "_"); + } else { + templateName = java.util.UUID.nameUUIDFromBytes((jobs.get(jobId).getTmpltName() + System.currentTimeMillis()).getBytes()).toString(); + } + + String templateFilename = templateName + "." + extension; + dnld.setTmpltPath(finalTemplatePath + "/" + templateFilename); + scr.add("-n", templateFilename); + + scr.add("-t", templatePath); + scr.add("-f", td.getDownloadLocalPath()); + if (dnld.getChecksum() != null && dnld.getChecksum().length() > 1) { + scr.add("-c", dnld.getChecksum()); + } + scr.add("-u"); // cleanup + String result; + result = scr.execute(); + + if (result != null) { + return result; + } + + // Set permissions for the downloaded template + File downloadedTemplate = new File(templatePath + "/" + templateFilename); + _storage.setWorldReadableAndWriteable(downloadedTemplate); + + // Set permissions for template.properties + File templateProperties = new File(templatePath + "/template.properties"); + _storage.setWorldReadableAndWriteable(templateProperties); + + TemplateLocation loc = new TemplateLocation(_storage, templatePath); + try { + loc.create(dnld.getId(), true, dnld.getTmpltName()); + } catch (IOException e) { + s_logger.warn("Something is wrong with template location " + templatePath, e); + loc.purge(); + return "Unable to download due to " + e.getMessage(); + } + + Enumeration en = _processors.enumeration(); + while (en.hasMoreElements()) { + Processor processor = en.nextElement(); + + FormatInfo info = null; + try { + info = processor.process(templatePath, null, templateName); + } catch (InternalErrorException e) { + return e.toString(); + } + if (info != null) { + loc.addFormat(info); + dnld.setTemplatesize(info.virtualSize); + break; + } + } + + if (!loc.save()) { + s_logger.warn("Cleaning up because we're unable to save the formats"); + loc.purge(); + } + + return null; + } + + @Override + public Status getDownloadStatus(String jobId) { + DownloadJob job = jobs.get(jobId); + if (job != null) { + TemplateDownloader td = job.getTemplateDownloader(); + if (td != null) { + return td.getStatus(); + } + } + return Status.UNKNOWN; + } + + @Override + public String downloadPublicTemplate(long id, String url, String name, ImageFormat format, boolean hvm, Long accountId, String descr, String cksum, String installPathPrefix, String user, String password, long maxTemplateSizeInBytes) { + UUID uuid = new UUID(); + String jobId = uuid.toString(); + String tmpDir = installPathPrefix + File.separator + accountId + File.separator + id; + + try { + + if (!_storage.mkdirs(tmpDir)) { + s_logger.warn("Unable to create " + tmpDir); + return "Unable to create " + tmpDir; + } + + File file = _storage.getFile(tmpDir + File.separator + TemplateLocation.Filename); + + if (!file.createNewFile()) { + s_logger.warn("Unable to create new file: " + file.getAbsolutePath()); + return "Unable to create new file: " + file.getAbsolutePath(); + } + + URI uri; + try { + uri = new URI(url); + } catch (URISyntaxException e) { + throw new CloudRuntimeException("URI is incorrect: " + url); + } + TemplateDownloader td; + if ((uri != null) && (uri.getScheme() != null)) { + if (uri.getScheme().equalsIgnoreCase("http") || uri.getScheme().equalsIgnoreCase("https")) { + td = new HttpTemplateDownloader(_storage, url, tmpDir, new Completion(jobId), maxTemplateSizeInBytes, user, password); + } else if (uri.getScheme().equalsIgnoreCase("file")) { + td = new LocalTemplateDownloader(_storage, url, tmpDir, maxTemplateSizeInBytes, new Completion(jobId)); + } else if (uri.getScheme().equalsIgnoreCase("scp")) { + td = new ScpTemplateDownloader(_storage, url, tmpDir, maxTemplateSizeInBytes, new Completion(jobId)); + } else if (uri.getScheme().equalsIgnoreCase("nfs")) { + td = null; + // TODO: implement this. + throw new CloudRuntimeException("Scheme is not supported " + url); + } else { + throw new CloudRuntimeException("Scheme is not supported " + url); + } + } else { + throw new CloudRuntimeException("Unable to download from URL: " + url); + } + DownloadJob dj = new DownloadJob(td, jobId, id, name, format, hvm, accountId, descr, cksum, installPathPrefix); + jobs.put(jobId, dj); + threadPool.execute(td); + + return jobId; + } catch (IOException e) { + s_logger.warn("Unable to download to " + tmpDir, e); + return null; + } + } + + public String getPublicTemplateRepo() { + return publicTemplateRepo; + } + + @Override + public String getDownloadError(String jobId) { + DownloadJob dj = jobs.get(jobId); + if (dj != null) { + return dj.getTemplateDownloader().getDownloadError(); + } + return null; + } + + public long getDownloadTemplateSize(String jobId) { + DownloadJob dj = jobs.get(jobId); + if (dj != null) { + return dj.getTemplatesize(); + } + return 0; + } + + // @Override + public String getDownloadLocalPath(String jobId) { + DownloadJob dj = jobs.get(jobId); + if (dj != null) { + return dj.getTemplateDownloader().getDownloadLocalPath(); + } + return null; + } + + @Override + public int getDownloadPct(String jobId) { + DownloadJob dj = jobs.get(jobId); + if (dj != null) { + return dj.getTemplateDownloader().getDownloadPercent(); + } + return 0; + } + + public static VMTemplateHostVO.Status convertStatus(Status tds) { + switch (tds) { + case ABORTED: + return VMTemplateHostVO.Status.NOT_DOWNLOADED; + case DOWNLOAD_FINISHED: + return VMTemplateHostVO.Status.DOWNLOAD_IN_PROGRESS; + case IN_PROGRESS: + return VMTemplateHostVO.Status.DOWNLOAD_IN_PROGRESS; + case NOT_STARTED: + return VMTemplateHostVO.Status.NOT_DOWNLOADED; + case RECOVERABLE_ERROR: + return VMTemplateHostVO.Status.NOT_DOWNLOADED; + case UNKNOWN: + return VMTemplateHostVO.Status.UNKNOWN; + case UNRECOVERABLE_ERROR: + return VMTemplateHostVO.Status.DOWNLOAD_ERROR; + case POST_DOWNLOAD_FINISHED: + return VMTemplateHostVO.Status.DOWNLOADED; + default: + return VMTemplateHostVO.Status.UNKNOWN; + } + } + + @Override + public com.cloud.storage.VMTemplateHostVO.Status getDownloadStatus2(String jobId) { + return convertStatus(getDownloadStatus(jobId)); + } + + @Override + public DownloadAnswer handleDownloadCommand(DownloadCommand cmd) { + if (cmd instanceof DownloadProgressCommand) { + return handleDownloadProgressCmd((DownloadProgressCommand) cmd); + } + + if (cmd.getUrl() == null) { + return new DownloadAnswer(null, 0, "Template is corrupted on storage due to an invalid url , cannot download", com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR, "", "", 0); + } + + if (cmd.getName() == null) { + return new DownloadAnswer(null, 0, "Invalid Name", com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR, "", "", 0); + } + + String installPathPrefix = null; + installPathPrefix = publicTemplateRepo; + + String user = null; + String password = null; + if (cmd.getAuth() != null) { + user = cmd.getAuth().getUserName(); + password = new String(cmd.getAuth().getPassword()); + } + + long maxDownloadSizeInBytes = (cmd.getMaxDownloadSizeInBytes() == null) ? TemplateDownloader.DEFAULT_MAX_TEMPLATE_SIZE_IN_BYTES : (cmd.getMaxDownloadSizeInBytes()); + String jobId = downloadPublicTemplate(cmd.getId(), cmd.getUrl(), cmd.getName(), cmd.getFormat(), cmd.isHvm(), cmd.getAccountId(), cmd.getDescription(), cmd.getChecksum(), installPathPrefix, user, password, maxDownloadSizeInBytes); + sleep(); + if (jobId == null) { + return new DownloadAnswer(null, 0, "Internal Error", com.cloud.storage.VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR, "", "", 0); + } + return new DownloadAnswer(jobId, getDownloadPct(jobId), getDownloadError(jobId), getDownloadStatus2(jobId), getDownloadLocalPath(jobId), getInstallPath(jobId), + getDownloadTemplateSize(jobId)); + } + + private void sleep() { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + // ignore + } + } + + private DownloadAnswer handleDownloadProgressCmd(DownloadProgressCommand cmd) { + String jobId = cmd.getJobId(); + DownloadAnswer answer; + DownloadJob dj = null; + if (jobId != null) + dj = jobs.get(jobId); + if (dj == null) { + if (cmd.getRequest() == RequestType.GET_OR_RESTART) { + DownloadCommand dcmd = new DownloadCommand(cmd); + return handleDownloadCommand(dcmd); + } else { + return new DownloadAnswer(null, 0, "Cannot find job", com.cloud.storage.VMTemplateHostVO.Status.UNKNOWN, "", "", 0); + } + } + TemplateDownloader td = dj.getTemplateDownloader(); + switch (cmd.getRequest()) { + case GET_STATUS: + break; + case ABORT: + td.stopDownload(); + sleep(); + break; + case RESTART: + td.stopDownload(); + sleep(); + threadPool.execute(td); + break; + case PURGE: + td.stopDownload(); + answer = new DownloadAnswer(jobId, getDownloadPct(jobId), getDownloadError(jobId), getDownloadStatus2(jobId), getDownloadLocalPath(jobId), getInstallPath(jobId), getDownloadTemplateSize(jobId)); + jobs.remove(jobId); + return answer; + default: + break; // TODO + } + return new DownloadAnswer(jobId, getDownloadPct(jobId), getDownloadError(jobId), getDownloadStatus2(jobId), getDownloadLocalPath(jobId), getInstallPath(jobId), + getDownloadTemplateSize(jobId)); + } + + private String getInstallPath(String jobId) { + DownloadJob dj = jobs.get(jobId); + if (dj != null) { + return dj.getTmpltPath(); + } + return null; + } + + private String createTempDir(File rootDir, String name) throws IOException { + + File f = File.createTempFile(name, "", rootDir); + f.delete(); + f.mkdir(); + _storage.setWorldReadableAndWriteable(f); + return f.getAbsolutePath(); + + } + + @Override + public List listPublicTemplates() { + return listTemplates(publicTemplateRepo); + } + + private List listTemplates(String rootdir) { + List result = new ArrayList(); + Script script = new Script(listTmpltScr, s_logger); + script.add("-r", rootdir); + ZfsPathParser zpp = new ZfsPathParser(rootdir); + script.execute(zpp); + result.addAll(zpp.getPaths()); + s_logger.info("found " + zpp.getPaths().size() + " templates"); + return result; + } + + @Override + public Map gatherTemplateInfo() { + Map result = new HashMap(); + List publicTmplts = listPublicTemplates(); + for (String tmplt : publicTmplts) { + String path = tmplt.substring(0, tmplt.lastIndexOf(File.separator)); + TemplateLocation loc = new TemplateLocation(_storage, path); + try { + if (!loc.load()) { + s_logger.warn("Post download installation was not completed for " + path); + loc.purge(); + _storage.cleanup(path, publicTemplateRepo); + continue; + } + } catch (IOException e) { + s_logger.warn("Unable to load template location " + path, e); + loc.purge(); + try { + _storage.cleanup(path, publicTemplateRepo); + } catch (IOException e1) { + s_logger.warn("Unable to cleanup " + path, e1); + } + continue; + } + + TemplateInfo tInfo = loc.getTemplateInfo(); + + result.put(tInfo.templateName, tInfo); + s_logger.debug("Added template name: " + tInfo.templateName + ", path: " + tmplt); + } + /* + for (String tmplt : isoTmplts) { + String tmp[]; + tmp = tmplt.split("/"); + String tmpltName = tmp[tmp.length - 2]; + tmplt = tmplt.substring(tmplt.lastIndexOf("iso/")); + TemplateInfo tInfo = new TemplateInfo(tmpltName, tmplt, false); + s_logger.debug("Added iso template name: " + tmpltName + ", path: " + tmplt); + result.put(tmpltName, tInfo); + } + */ + return result; + } + + private int deleteDownloadDirectories(File downloadPath, int deleted) { + try { + if (downloadPath.exists()) { + File[] files = downloadPath.listFiles(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + deleteDownloadDirectories(files[i], deleted); + files[i].delete(); + deleted++; + } else { + files[i].delete(); + deleted++; + } + } + } + } catch (Exception ex) { + s_logger.info("Failed to clean up template downloads directory " + ex.toString()); + } + return deleted; + } + + public static class ZfsPathParser extends OutputInterpreter { + String _parent; + List paths = new ArrayList(); + + public ZfsPathParser(String parent) { + _parent = parent; + } + + @Override + public String interpret(BufferedReader reader) throws IOException { + String line = null; + while ((line = reader.readLine()) != null) { + paths.add(line); + } + return null; + } + + public List getPaths() { + return paths; + } + } + + public DownloadManagerImpl() { + } + + @Override + @SuppressWarnings("unchecked") + public boolean configure(String name, Map params) throws ConfigurationException { + _name = name; + + String value = null; + + _storage = (StorageLayer) params.get(StorageLayer.InstanceConfigKey); + if (_storage == null) { + value = (String) params.get(StorageLayer.ClassConfigKey); + if (value == null) { + throw new ConfigurationException("Unable to find the storage layer"); + } + + Class clazz; + try { + clazz = (Class) Class.forName(value); + } catch (ClassNotFoundException e) { + throw new ConfigurationException("Unable to instantiate " + value); + } + _storage = ComponentLocator.inject(clazz); + } + String useSsl = (String)params.get("sslcopy"); + if (useSsl != null) { + _sslCopy = Boolean.parseBoolean(useSsl); + + } + configureFolders(name, params); + String inSystemVM = (String)params.get("secondary.storage.vm"); + if (inSystemVM != null && "true".equalsIgnoreCase(inSystemVM)) { + s_logger.info("DownloadManager: starting additional services since we are inside system vm"); + startAdditionalServices(); + blockOutgoingOnPrivate(); + } + + value = (String) params.get("install.timeout.pergig"); + this.installTimeoutPerGig = NumbersUtil.parseInt(value, 15 * 60) * 1000; + + value = (String) params.get("install.numthreads"); + final int numInstallThreads = NumbersUtil.parseInt(value, 10); + + String scriptsDir = (String) params.get("template.scripts.dir"); + if (scriptsDir == null) { + scriptsDir = "scripts/storage/secondary"; + } + + listTmpltScr = Script.findScript(scriptsDir, "listvmtmplt.sh"); + if (listTmpltScr == null) { + throw new ConfigurationException("Unable to find the listvmtmplt.sh"); + } + s_logger.info("listvmtmplt.sh found in " + listTmpltScr); + + createTmpltScr = Script.findScript(scriptsDir, "createtmplt.sh"); + if (createTmpltScr == null) { + throw new ConfigurationException("Unable to find createtmplt.sh"); + } + s_logger.info("createtmplt.sh found in " + createTmpltScr); + + List processors = new ArrayList(); + _processors = new Adapters("processors", processors); + Processor processor = new VhdProcessor(); + + processor.configure("VHD Processor", params); + processors.add(processor); + + processor = new IsoProcessor(); + processor.configure("ISO Processor", params); + processors.add(processor); + + processor = new QCOW2Processor(); + processor.configure("QCOW2 Processor", params); + processors.add(processor); + // Add more processors here. + threadPool = Executors.newFixedThreadPool(numInstallThreads); + return true; + } + + private void blockOutgoingOnPrivate() { + Script command = new Script("/bin/bash", s_logger); + String intf = "eth1"; + command.add("-c"); + command.add("iptables -A OUTPUT -o " + intf + " -p tcp -m state --state NEW -m tcp --dport " + "80" + " -j REJECT;" + + "iptables -A OUTPUT -o " + intf + " -p tcp -m state --state NEW -m tcp --dport " + "443" + " -j REJECT;"); + + String result = command.execute(); + if (result != null) { + s_logger.warn("Error in blocking outgoing to port 80/443 err=" + result ); + return; + } + } + + protected void configureFolders(String name, Map params) throws ConfigurationException { + parentDir = (String) params.get("template.parent"); + if (parentDir == null) { + throw new ConfigurationException("Unable to find the parent root for the templates"); + } + + String value = (String) params.get("public.templates.root.dir"); + if (value == null) { + value = TemplateConstants.DEFAULT_TMPLT_ROOT_DIR; + } + + if (value.startsWith(File.separator)) { + publicTemplateRepo = value; + } else { + publicTemplateRepo = parentDir + File.separator + value; + } + + if (!publicTemplateRepo.endsWith(File.separator)) { + publicTemplateRepo += File.separator; + } + + publicTemplateRepo += TemplateConstants.DEFAULT_TMPLT_FIRST_LEVEL_DIR; + + if (!_storage.mkdirs(publicTemplateRepo)) { + throw new ConfigurationException("Unable to create public templates directory"); + } + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } + + private void startAdditionalServices() { + + Script command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add("service httpd stop "); + String result = command.execute(); + if (result != null) { + s_logger.warn("Error in stopping httpd service err=" + result ); + } + String port = Integer.toString(TemplateConstants.DEFAULT_TMPLT_COPY_PORT); + String intf = TemplateConstants.DEFAULT_TMPLT_COPY_INTF; + + command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add("iptables -D INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + port + " -j DROP;" + + "iptables -D INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + port + " -j HTTP;" + + "iptables -D INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + "443" + " -j DROP;" + + "iptables -D INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + "443" + " -j HTTP;" + + "iptables -F HTTP;" + + "iptables -X HTTP;" + + "iptables -N HTTP;" + + "iptables -I INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + port + " -j DROP;" + + "iptables -I INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + "443" + " -j DROP;" + + "iptables -I INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + port + " -j HTTP;" + + "iptables -I INPUT -i " + intf + " -p tcp -m state --state NEW -m tcp --dport " + "443" + " -j HTTP;"); + + result = command.execute(); + if (result != null) { + s_logger.warn("Error in opening up httpd port err=" + result ); + return; + } + + command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add("service httpd start "); + result = command.execute(); + if (result != null) { + s_logger.warn("Error in starting httpd service err=" + result ); + return; + } + command = new Script("mkdir", s_logger); + command.add("-p"); + command.add("/var/www/html/copy/template"); + result = command.execute(); + if (result != null) { + s_logger.warn("Error in creating directory =" + result ); + return; + } + + command = new Script("/bin/bash", s_logger); + command.add("-c"); + command.add("ln -sf " + publicTemplateRepo + " /var/www/html/copy/template"); + result = command.execute(); + if (result != null) { + s_logger.warn("Error in linking err=" + result ); + return; + } + } +} diff --git a/core/src/com/cloud/storage/template/HttpTemplateDownloader.java b/core/src/com/cloud/storage/template/HttpTemplateDownloader.java new file mode 100644 index 00000000000..8ffb0c1308b --- /dev/null +++ b/core/src/com/cloud/storage/template/HttpTemplateDownloader.java @@ -0,0 +1,450 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.Date; + +import org.apache.commons.httpclient.ChunkedInputStream; +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpMethodRetryHandler; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.NoHttpResponseException; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.params.HttpMethodParams; +import org.apache.log4j.Logger; + +import com.cloud.storage.StorageLayer; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.Pair; + +/** + * Download a template file using HTTP + * @author Chiradeep + * + */ +public class HttpTemplateDownloader implements TemplateDownloader { + public static final Logger s_logger = Logger.getLogger(HttpTemplateDownloader.class.getName()); + + private static final int CHUNK_SIZE = 1024*1024; //1M + private String downloadUrl; + private String toFile; + public TemplateDownloader.Status status= TemplateDownloader.Status.NOT_STARTED; + public String errorString = " "; + private long remoteSize = 0; + public long downloadTime = 0; + public long totalBytes; + private final HttpClient client; + private GetMethod request; + private boolean resume = false; + private DownloadCompleteCallback completionCallback; + StorageLayer _storage; + boolean inited = true; + + private String toDir; + private long MAX_TEMPLATE_SIZE_IN_BYTES; + + private final HttpMethodRetryHandler myretryhandler; + + public HttpTemplateDownloader (StorageLayer storageLayer, String downloadUrl, String toDir, DownloadCompleteCallback callback, long maxTemplateSizeInBytes, String user, String password) { + this._storage = storageLayer; + this.downloadUrl = downloadUrl; + this.setToDir(toDir); + this.status = TemplateDownloader.Status.NOT_STARTED; + + this.MAX_TEMPLATE_SIZE_IN_BYTES = maxTemplateSizeInBytes; + + this.totalBytes = 0; + this.client = new HttpClient(); + + myretryhandler = new HttpMethodRetryHandler() { + public boolean retryMethod( + final HttpMethod method, + final IOException exception, + int executionCount) { + if (executionCount >= 2) { + // Do not retry if over max retry count + return false; + } + if (exception instanceof NoHttpResponseException) { + // Retry if the server dropped connection on us + return true; + } + if (!method.isRequestSent()) { + // Retry if the request has not been sent fully or + // if it's OK to retry methods that have been sent + return true; + } + // otherwise do not retry + return false; + } + }; + + try { + this.request = new GetMethod(downloadUrl); + this.request.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, myretryhandler); + this.completionCallback = callback; + //this.request.setFollowRedirects(false); + + File f = File.createTempFile("dnld", "tmp_", new File(toDir)); + + if (_storage != null) { + _storage.setWorldReadableAndWriteable(f); + } + + toFile = f.getAbsolutePath(); + Pair hostAndPort = validateUrl(downloadUrl); + + if ((user != null) && (password != null)) { + client.getParams().setAuthenticationPreemptive(true); + Credentials defaultcreds = new UsernamePasswordCredentials(user, password); + client.getState().setCredentials(new AuthScope(hostAndPort.first(), hostAndPort.second(), AuthScope.ANY_REALM), defaultcreds); + s_logger.info("Added username=" + user + ", password=" + password + "for host " + hostAndPort.first() + ":" + hostAndPort.second()); + } else { + s_logger.info("No credentials configured for host=" + hostAndPort.first() + ":" + hostAndPort.second()); + } + } catch (IllegalArgumentException iae) { + errorString = iae.getMessage(); + status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; + inited = false; + } catch (Exception ex){ + errorString = "Unable to start download -- check url? "; + status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; + s_logger.warn("Exception in constructor -- " + ex.toString()); + } catch (Throwable th) { + s_logger.warn("throwable caught ", th); + } + } + + + private Pair validateUrl(String url) throws IllegalArgumentException { + try { + URI uri = new URI(url); + if (!uri.getScheme().equalsIgnoreCase("http") && !uri.getScheme().equalsIgnoreCase("https") ) { + throw new IllegalArgumentException("Unsupported scheme for url"); + } + int port = uri.getPort(); + if (!(port == 80 || port == 443 || port == -1)) { + throw new IllegalArgumentException("Only ports 80 and 443 are allowed"); + } + + if (port == -1 && uri.getScheme().equalsIgnoreCase("https")) { + port = 443; + } else if (port == -1 && uri.getScheme().equalsIgnoreCase("http")) { + port = 80; + } + + String host = uri.getHost(); + try { + InetAddress hostAddr = InetAddress.getByName(host); + if (hostAddr.isAnyLocalAddress() || hostAddr.isLinkLocalAddress() || hostAddr.isLoopbackAddress() || hostAddr.isMulticastAddress()) { + throw new IllegalArgumentException("Illegal host specified in url"); + } + if (hostAddr instanceof Inet6Address) { + throw new IllegalArgumentException("IPV6 addresses not supported (" + hostAddr.getHostAddress() + ")"); + } + return new Pair(host, port); + } catch (UnknownHostException uhe) { + throw new IllegalArgumentException("Unable to resolve " + host); + } + } catch (IllegalArgumentException iae) { + s_logger.warn("Failed uri validation check: " + iae.getMessage()); + throw iae; + } catch (URISyntaxException use) { + s_logger.warn("Failed uri syntax check: " + use.getMessage()); + throw new IllegalArgumentException(use.getMessage()); + } + } + + @Override + public long download(boolean resume, DownloadCompleteCallback callback) { + switch (status) { + case ABORTED: + case UNRECOVERABLE_ERROR: + case DOWNLOAD_FINISHED: + return 0; + default: + + } + int bytes=0; + File file = new File(toFile); + try { + + long localFileSize = 0; + if (file.exists() && resume) { + localFileSize = file.length(); + s_logger.info("Resuming download to file (current size)=" + localFileSize); + } + + Date start = new Date(); + + int responseCode=0; + + if (localFileSize > 0 ) { + // require partial content support for resume + request.addRequestHeader("Range", "bytes=" + localFileSize + "-"); + if (client.executeMethod(request) != HttpStatus.SC_PARTIAL_CONTENT) { + errorString = "HTTP Server does not support partial get"; + status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; + return 0; + } + } else if ((responseCode = client.executeMethod(request)) != HttpStatus.SC_OK) { + status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; + errorString = " HTTP Server returned " + responseCode + " (expected 200 OK) "; + return 0; //FIXME: retry? + } + + Header contentLengthHeader = request.getResponseHeader("Content-Length"); + boolean chunked = false; + long remoteSize2 = 0; + if (contentLengthHeader == null) { + Header chunkedHeader = request.getResponseHeader("Transfer-Encoding"); + if (chunkedHeader == null || !"chunked".equalsIgnoreCase(chunkedHeader.getValue())) { + status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; + errorString=" Failed to receive length of download "; + return 0; //FIXME: what status do we put here? Do we retry? + } else if ("chunked".equalsIgnoreCase(chunkedHeader.getValue())){ + chunked = true; + } + } else { + remoteSize2 = Long.parseLong(contentLengthHeader.getValue()); + } + + if (remoteSize == 0) { + remoteSize = remoteSize2; + } + + if (remoteSize > MAX_TEMPLATE_SIZE_IN_BYTES) { + s_logger.info("Remote size is too large: " + remoteSize + " , max=" + MAX_TEMPLATE_SIZE_IN_BYTES); + status = Status.UNRECOVERABLE_ERROR; + errorString = "Download file size is too large"; + return 0; + } + + if (remoteSize == 0) { + remoteSize = MAX_TEMPLATE_SIZE_IN_BYTES; + } + + InputStream in = !chunked?new BufferedInputStream(request.getResponseBodyAsStream()) + : new ChunkedInputStream(request.getResponseBodyAsStream()); + + RandomAccessFile out = new RandomAccessFile(file, "rwd"); + out.seek(localFileSize); + + s_logger.info("Starting download from " + getDownloadUrl() + " to " + toFile + " remoteSize=" + remoteSize + " , max size=" + MAX_TEMPLATE_SIZE_IN_BYTES); + + byte[] block = new byte[CHUNK_SIZE]; + long offset=0; + boolean done=false; + status = TemplateDownloader.Status.IN_PROGRESS; + while (!done && status != Status.ABORTED && offset <= remoteSize) { + if ( (bytes = in.read(block, 0, CHUNK_SIZE)) > -1) { + out.write(block, 0, bytes); + offset +=bytes; + out.seek(offset); + totalBytes += bytes; + } else { + done = true; + } + } + Date finish = new Date(); + String downloaded = "(incomplete download)"; + if (totalBytes >= remoteSize) { + status = TemplateDownloader.Status.DOWNLOAD_FINISHED; + downloaded = "(download complete remote=" + remoteSize + "bytes)"; + } + errorString = "Downloaded " + totalBytes + " bytes " + downloaded; + downloadTime += finish.getTime() - start.getTime(); + out.close(); + + return totalBytes; + }catch (HttpException hte) { + status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; + errorString = hte.getMessage(); + } catch (IOException ioe) { + status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; //probably a file write error? + errorString = ioe.getMessage(); + } finally { + if (status == Status.UNRECOVERABLE_ERROR && file.exists() && !file.isDirectory()) { + file.delete(); + } + request.releaseConnection(); + if (callback != null) { + callback.downloadComplete(status); + } + } + return 0; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public String getToFile() { + File file = new File(toFile); + + return file.getAbsolutePath(); + } + + public TemplateDownloader.Status getStatus() { + return status; + } + + + public long getDownloadTime() { + return downloadTime; + } + + + public long getDownloadedBytes() { + return totalBytes; + } + + @Override + @SuppressWarnings("fallthrough") + public boolean stopDownload() { + switch (getStatus()) { + case IN_PROGRESS: + if (request != null) { + request.abort(); + } + status = TemplateDownloader.Status.ABORTED; + return true; + case UNKNOWN: + case NOT_STARTED: + case RECOVERABLE_ERROR: + case UNRECOVERABLE_ERROR: + case ABORTED: + status = TemplateDownloader.Status.ABORTED; + case DOWNLOAD_FINISHED: + File f = new File(toFile); + if (f.exists()) { + f.delete(); + } + return true; + + default: + return true; + } + } + + @Override + public int getDownloadPercent() { + if (remoteSize == 0) { + return 0; + } + + return (int)(100.0*totalBytes/remoteSize); + } + + @Override + public void run() { + try { + download(resume, completionCallback); + } catch (Throwable t) { + s_logger.warn("Caught exception during download "+ t.getMessage(), t); + errorString = "Failed to install: " + t.getMessage(); + status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; + } + + } + + @Override + public void setStatus(TemplateDownloader.Status status) { + this.status = status; + } + + + + public boolean isResume() { + return resume; + } + + @Override + public String getDownloadError() { + return errorString; + } + + @Override + public String getDownloadLocalPath() { + return getToFile(); + } + + public void setResume(boolean resume) { + this.resume = resume; + } + + public void setToDir(String toDir) { + this.toDir = toDir; + } + + public String getToDir() { + return toDir; + } + + public long getMaxTemplateSizeInBytes() { + return this.MAX_TEMPLATE_SIZE_IN_BYTES; + } + + public static void main(String[] args) { + String url ="http:// dev.mysql.com/get/Downloads/MySQL-5.0/mysql-noinstall-5.0.77-win32.zip/from/http://mirror.services.wisc.edu/mysql/"; + try { + URI uri = new java.net.URI(url); + } catch (URISyntaxException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + TemplateDownloader td = new HttpTemplateDownloader(null, url,"/tmp/mysql", null, TemplateDownloader.DEFAULT_MAX_TEMPLATE_SIZE_IN_BYTES, null, null); + long bytes = td.download(true, null); + if (bytes > 0) { + System.out.println("Downloaded (" + bytes + " bytes)" + " in " + td.getDownloadTime()/1000 + " secs"); + } else { + System.out.println("Failed download"); + } + + } + + @Override + public void setDownloadError(String error) { + errorString = error; + } + + + + @Override + public boolean isInited() { + return inited; + } + +} diff --git a/core/src/com/cloud/storage/template/IsoProcessor.java b/core/src/com/cloud/storage/template/IsoProcessor.java new file mode 100644 index 00000000000..afbb1b47663 --- /dev/null +++ b/core/src/com/cloud/storage/template/IsoProcessor.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +import java.io.File; +import java.util.Map; + +import javax.ejb.Local; +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.storage.StorageLayer; +import com.cloud.storage.Storage.ImageFormat; + +@Local(value=Processor.class) +public class IsoProcessor implements Processor { + private static final Logger s_logger = Logger.getLogger(IsoProcessor.class); + + String _name; + StorageLayer _storage; + + @Override + public FormatInfo process(String templatePath, ImageFormat format, String templateName) { + if (format != null) { + s_logger.debug("We don't handle conversion from " + format + " to ISO."); + return null; + } + + String isoPath = templatePath + File.separator + templateName + "." + ImageFormat.ISO.getFileExtension(); + + if (!_storage.exists(isoPath)) { + s_logger.debug("Unable to find the iso file: " + isoPath); + return null; + } + + FormatInfo info = new FormatInfo(); + info.format = ImageFormat.ISO; + info.filename = templateName + "." + ImageFormat.ISO.getFileExtension(); + info.size = _storage.getSize(isoPath); + info.virtualSize = info.size; + + return info; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + _name = name; + _storage = (StorageLayer)params.get(StorageLayer.InstanceConfigKey); + if (_storage == null) { + throw new ConfigurationException("Unable to get storage implementation"); + } + return true; + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } +} diff --git a/core/src/com/cloud/storage/template/LocalTemplateDownloader.java b/core/src/com/cloud/storage/template/LocalTemplateDownloader.java new file mode 100644 index 00000000000..ecd330a4615 --- /dev/null +++ b/core/src/com/cloud/storage/template/LocalTemplateDownloader.java @@ -0,0 +1,157 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import org.apache.log4j.Logger; + +import com.cloud.storage.StorageLayer; + + + +public class LocalTemplateDownloader extends TemplateDownloaderBase implements TemplateDownloader { + public static final Logger s_logger = Logger.getLogger(LocalTemplateDownloader.class); + + public LocalTemplateDownloader(StorageLayer storageLayer, String downloadUrl, String toDir, long maxTemplateSizeInBytes, DownloadCompleteCallback callback) { + super(storageLayer, downloadUrl, toDir, maxTemplateSizeInBytes, callback); + String filename = downloadUrl.substring(downloadUrl.lastIndexOf(File.separator)); + _toFile = toDir.endsWith(File.separator) ? (toDir + filename) : (toDir + File.separator + filename); + } + + @Override + public long download(boolean resume, DownloadCompleteCallback callback) { + if (_status == Status.ABORTED || + _status == Status.UNRECOVERABLE_ERROR || + _status == Status.DOWNLOAD_FINISHED) { + return 0; + } + + _start = System.currentTimeMillis(); + _resume = resume; + + File src; + try { + src = new File(new URI(_downloadUrl)); + } catch (URISyntaxException e1) { + s_logger.warn("Invalid URI " + _downloadUrl); + _status = Status.UNRECOVERABLE_ERROR; + return 0; + } + File dst = new File(_toFile); + + FileChannel fic = null; + FileChannel foc = null; + + try { + if (_storage != null) { + dst.createNewFile(); + _storage.setWorldReadableAndWriteable(dst); + } + + ByteBuffer buffer = ByteBuffer.allocate(1024 * 512); + FileInputStream fis; + try { + fis = new FileInputStream(src); + } catch (FileNotFoundException e) { + s_logger.warn("Unable to find " + _downloadUrl); + _errorString = "Unable to find " + _downloadUrl; + return -1; + } + fic = fis.getChannel(); + FileOutputStream fos; + try { + fos = new FileOutputStream(dst); + } catch (FileNotFoundException e) { + s_logger.warn("Unable to find " + _toFile); + return -1; + } + foc = fos.getChannel(); + + _remoteSize = src.length(); + this._totalBytes = 0; + _status = TemplateDownloader.Status.IN_PROGRESS; + + try { + while (_status != Status.ABORTED && fic.read(buffer) != -1) { + buffer.flip(); + int count = foc.write(buffer); + _totalBytes += count; + buffer.clear(); + } + } catch (IOException e) { + s_logger.warn("Unable to download", e); + } + + String downloaded = "(incomplete download)"; + if (_totalBytes == _remoteSize) { + _status = TemplateDownloader.Status.DOWNLOAD_FINISHED; + downloaded = "(download complete)"; + } + + _errorString = "Downloaded " + _remoteSize + " bytes " + downloaded; + _downloadTime += System.currentTimeMillis() - _start; + return _totalBytes; + } catch (Exception e) { + _status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; + _errorString = e.getMessage(); + return 0; + } finally { + if (fic != null) { + try { + fic.close(); + } catch (IOException e) { + } + } + + if (foc != null) { + try { + foc.close(); + } catch (IOException e) { + } + } + + if (_status == Status.UNRECOVERABLE_ERROR && dst.exists()) { + dst.delete(); + } + if (callback != null) { + callback.downloadComplete(_status); + } + } + } + + public static void main(String[] args) { + String url ="file:///home/ahuang/Download/E3921_P5N7A-VM_manual.zip"; + TemplateDownloader td = new LocalTemplateDownloader(null, url,"/tmp/mysql", TemplateDownloader.DEFAULT_MAX_TEMPLATE_SIZE_IN_BYTES, null); + long bytes = td.download(true, null); + if (bytes > 0) { + System.out.println("Downloaded (" + bytes + " bytes)" + " in " + td.getDownloadTime()/1000 + " secs"); + } else { + System.out.println("Failed download"); + } + + } +} diff --git a/core/src/com/cloud/storage/template/Processor.java b/core/src/com/cloud/storage/template/Processor.java new file mode 100644 index 00000000000..43e82a682e4 --- /dev/null +++ b/core/src/com/cloud/storage/template/Processor.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +import com.cloud.exception.InternalErrorException; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.utils.component.Adapter; + +/** + * Generic interface to process different types of image formats + * for templates downloaded and for conversion from one format + * to anther. + * + */ +public interface Processor extends Adapter { + + /** + * Returns image format if it was able to process the original file and + * change it to the the image format it requires. + * + * @param templatePath path to the templates to process. + * @param format Format of the original file. If null, it means unknown. If not null, + * there is already a file with thte template name and image format extension + * that exists in case a conversion can be done. + * @param templateName file name to call the resulting image file. The processor is required to add extensions. + * @return FormatInfo if the file is processed. null if not. + */ + FormatInfo process(String templatePath, ImageFormat format, String templateName) throws InternalErrorException; + + public static class FormatInfo { + public ImageFormat format; + public long size; + public long virtualSize; + public String filename; + } +} diff --git a/core/src/com/cloud/storage/template/QCOW2Processor.java b/core/src/com/cloud/storage/template/QCOW2Processor.java new file mode 100644 index 00000000000..6c7fbc6a3e2 --- /dev/null +++ b/core/src/com/cloud/storage/template/QCOW2Processor.java @@ -0,0 +1,114 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.storage.template; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Map; + +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.storage.StorageLayer; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.utils.NumbersUtil; + +public class QCOW2Processor implements Processor { + private static final Logger s_logger = Logger.getLogger(QCOW2Processor.class); + String _name; + StorageLayer _storage; + + @Override + public FormatInfo process(String templatePath, ImageFormat format, + String templateName) { + if (format != null) { + s_logger.debug("We currently don't handle conversion from " + format + " to QCOW2."); + return null; + } + + String qcow2Path = templatePath + File.separator + templateName + "." + ImageFormat.QCOW2.getFileExtension(); + + if (!_storage.exists(qcow2Path)) { + s_logger.debug("Unable to find the qcow2 file: " + qcow2Path); + return null; + } + + FormatInfo info = new FormatInfo(); + info.format = ImageFormat.QCOW2; + info.filename = templateName + "." + ImageFormat.QCOW2.getFileExtension(); + + File qcow2File = _storage.getFile(qcow2Path); + + info.size = _storage.getSize(qcow2Path); + FileInputStream strm = null; + byte[] b = new byte[8]; + try { + strm = new FileInputStream(qcow2File); + strm.skip(24); + strm.read(b); + } catch (Exception e) { + s_logger.warn("Unable to read qcow2 file " + qcow2Path, e); + return null; + } finally { + if (strm != null) { + try { + strm.close(); + } catch (IOException e) { + } + } + } + + long templateSize = NumbersUtil.bytesToLong(b); + info.virtualSize = templateSize; + + return info; + } + + @Override + public boolean configure(String name, Map params) + throws ConfigurationException { + _name = name; + _storage = (StorageLayer)params.get(StorageLayer.InstanceConfigKey); + if (_storage == null) { + throw new ConfigurationException("Unable to get storage implementation"); + } + + return true; + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + // TODO Auto-generated method stub + return true; + } + + @Override + public boolean stop() { + // TODO Auto-generated method stub + return true; + } + +} diff --git a/core/src/com/cloud/storage/template/ScpTemplateDownloader.java b/core/src/com/cloud/storage/template/ScpTemplateDownloader.java new file mode 100644 index 00000000000..a62a68f2cc7 --- /dev/null +++ b/core/src/com/cloud/storage/template/ScpTemplateDownloader.java @@ -0,0 +1,163 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; + +import org.apache.log4j.Logger; + +import com.cloud.storage.StorageLayer; +import com.cloud.utils.exception.CloudRuntimeException; +import com.trilead.ssh2.SCPClient; + +public class ScpTemplateDownloader extends TemplateDownloaderBase implements TemplateDownloader { + private static final Logger s_logger = Logger.getLogger(ScpTemplateDownloader.class); + + public ScpTemplateDownloader(StorageLayer storageLayer, String downloadUrl, String toDir, long maxTemplateSizeInBytes, DownloadCompleteCallback callback) { + super(storageLayer, downloadUrl, toDir, maxTemplateSizeInBytes, callback); + + URI uri; + try { + uri = new URI(_downloadUrl); + } catch (URISyntaxException e) { + s_logger.warn("URI syntax error: " + _downloadUrl); + _status = Status.UNRECOVERABLE_ERROR; + return; + } + + String path = uri.getPath(); + String filename = path.substring(path.lastIndexOf("/") + 1); + _toFile = toDir + File.separator + filename; + } + + @Override + public long download(boolean resume, DownloadCompleteCallback callback) { + if (_status == Status.ABORTED || + _status == Status.UNRECOVERABLE_ERROR || + _status == Status.DOWNLOAD_FINISHED) { + return 0; + } + + _resume = resume; + + _start = System.currentTimeMillis(); + + URI uri; + try { + uri = new URI(_downloadUrl); + } catch (URISyntaxException e1) { + _status = Status.UNRECOVERABLE_ERROR; + return 0; + } + + String username = uri.getUserInfo(); + String queries = uri.getQuery(); + String password = null; + if (queries != null) { + String[] qs = queries.split("&"); + for (String q : qs) { + String[] tokens = q.split("="); + if (tokens[0].equalsIgnoreCase("password")) { + password = tokens[1]; + break; + } + } + } + int port = uri.getPort(); + if (port == -1) { + port = 22; + } + long length = 0; + File file = new File(_toFile); + + com.trilead.ssh2.Connection sshConnection = new com.trilead.ssh2.Connection(uri.getHost(), port); + try { + if (_storage != null) { + file.createNewFile(); + _storage.setWorldReadableAndWriteable(file); + } + + sshConnection.connect(null, 60000, 60000); + if (!sshConnection.authenticateWithPassword(username, password)) { + throw new CloudRuntimeException("Unable to authenticate"); + } + + SCPClient scp = new SCPClient(sshConnection); + + String src = uri.getPath(); + + _status = Status.IN_PROGRESS; + scp.get(src, _toDir); + + if (!file.exists()) { + _status = Status.UNRECOVERABLE_ERROR; + s_logger.debug("unable to scp the file " + _downloadUrl); + return 0; + } + + _status = Status.DOWNLOAD_FINISHED; + + _totalBytes = file.length(); + + String downloaded = "(download complete)"; + + _errorString = "Downloaded " + _remoteSize + " bytes " + downloaded; + _downloadTime += System.currentTimeMillis() - _start; + return _totalBytes; + + } catch (Exception e) { + s_logger.warn("Unable to download " + _downloadUrl, e); + _status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; + _errorString = e.getMessage(); + return 0; + } finally { + sshConnection.close(); + if (_status == Status.UNRECOVERABLE_ERROR && file.exists()) { + file.delete(); + } + if (callback != null) { + callback.downloadComplete(_status); + } + } + } + + @Override + public int getDownloadPercent() { + if (_status == Status.DOWNLOAD_FINISHED) { + return 100; + } else if (_status == Status.IN_PROGRESS) { + return 50; + } else { + return 0; + } + } + + public static void main(String[] args) { + String url ="scp://root@sol10-2/root/alex/agent.zip?password=password"; + TemplateDownloader td = new ScpTemplateDownloader(null, url,"/tmp/mysql", TemplateDownloader.DEFAULT_MAX_TEMPLATE_SIZE_IN_BYTES, null); + long bytes = td.download(true, null); + if (bytes > 0) { + System.out.println("Downloaded (" + bytes + " bytes)" + " in " + td.getDownloadTime()/1000 + " secs"); + } else { + System.out.println("Failed download"); + } + + } +} diff --git a/core/src/com/cloud/storage/template/TemplateConstants.java b/core/src/com/cloud/storage/template/TemplateConstants.java new file mode 100644 index 00000000000..172a4e55d8e --- /dev/null +++ b/core/src/com/cloud/storage/template/TemplateConstants.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2010 VMOps, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +/** + * @author chiradeep + * + */ +public final class TemplateConstants { + public static final String DEFAULT_TMPLT_ROOT_DIR = "template/"; + public static final String DEFAULT_TMPLT_FIRST_LEVEL_DIR = "tmpl/"; + + public static final String DEFAULT_SYSTEM_VM_TEMPLATE_PATH = "template/tmpl/1/1/"; + public static final long DEFAULT_SYSTEM_VM_DB_ID = 1L; + public static final long DEFAULT_BUILTIN_VM_DB_ID = 2L; + + public static final String DEFAULT_SYSTEM_VM_TMPLT_NAME = "routing"; + + public static final int DEFAULT_TMPLT_COPY_PORT = 80; + public static final String DEFAULT_TMPLT_COPY_INTF = "eth2"; + + public static final String DEFAULT_SSL_CERT_DOMAIN = "realhostip.com"; + public static final String DEFAULT_HTTP_AUTH_USER = "cloud"; + +} diff --git a/core/src/com/cloud/storage/template/TemplateDownloader.java b/core/src/com/cloud/storage/template/TemplateDownloader.java new file mode 100644 index 00000000000..9dc65516673 --- /dev/null +++ b/core/src/com/cloud/storage/template/TemplateDownloader.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +/** + * Download a template file given a URL + * @author Chiradeep + * + */ +public interface TemplateDownloader extends Runnable{ + + + /** + * Callback used to notify completion of download + * @author chiradeep + * + */ + public interface DownloadCompleteCallback { + void downloadComplete( Status status); + + } + + public static enum Status {UNKNOWN, NOT_STARTED, IN_PROGRESS, ABORTED, UNRECOVERABLE_ERROR, RECOVERABLE_ERROR, DOWNLOAD_FINISHED, POST_DOWNLOAD_FINISHED} + + public static long DEFAULT_MAX_TEMPLATE_SIZE_IN_BYTES = 50L*1024L*1024L*1024L; + + /** + * Initiate download, resuming a previous one if required + * @param resume resume if necessary + * @param callback completion callback to be called after download is complete + * @return bytes downloaded + */ + public long download(boolean resume, DownloadCompleteCallback callback); + + /** + * @return + */ + public boolean stopDownload(); + + /** + * @return percent of file downloaded + */ + public int getDownloadPercent(); + + /** + * Get the status of the download + * @return status of download + */ + public TemplateDownloader.Status getStatus(); + + + /** + * Get time taken to download so far + * @return time in seconds taken to download + */ + public long getDownloadTime(); + + /** + * Get bytes downloaded + * @return bytes downloaded so far + */ + public long getDownloadedBytes(); + + /** + * Get the error if any + * @return error string if any + */ + public String getDownloadError(); + + /** Get local path of the downloaded file + * @return local path of the file downloaded + */ + public String getDownloadLocalPath(); + + public void setStatus(TemplateDownloader.Status status); + + public void setDownloadError(String string); + + public void setResume(boolean resume); + + public boolean isInited(); + + public long getMaxTemplateSizeInBytes(); + +} diff --git a/core/src/com/cloud/storage/template/TemplateDownloaderBase.java b/core/src/com/cloud/storage/template/TemplateDownloaderBase.java new file mode 100644 index 00000000000..14771fecfda --- /dev/null +++ b/core/src/com/cloud/storage/template/TemplateDownloaderBase.java @@ -0,0 +1,147 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +import java.io.File; + +import org.apache.log4j.Logger; + +import com.cloud.storage.StorageLayer; + +public abstract class TemplateDownloaderBase implements TemplateDownloader { + private static final Logger s_logger = Logger.getLogger(TemplateDownloaderBase.class); + + protected String _downloadUrl; + protected String _toFile; + protected TemplateDownloader.Status _status= TemplateDownloader.Status.NOT_STARTED; + protected String _errorString = " "; + protected long _remoteSize = 0; + protected long _downloadTime = 0; + protected long _totalBytes; + protected DownloadCompleteCallback _callback; + protected boolean _resume = false; + protected String _toDir; + protected long _start; + protected StorageLayer _storage; + protected boolean _inited = false; + private long MAX_TEMPLATE_SIZE_IN_BYTES; + + public TemplateDownloaderBase(StorageLayer storage, String downloadUrl, String toDir, long maxTemplateSizeInBytes, DownloadCompleteCallback callback) { + _storage = storage; + _downloadUrl = downloadUrl; + _toDir = toDir; + _callback = callback; + _inited = true; + + this.MAX_TEMPLATE_SIZE_IN_BYTES = maxTemplateSizeInBytes; + } + + @Override + public String getDownloadError() { + return _errorString; + } + + @Override + public String getDownloadLocalPath() { + File file = new File(_toFile); + return file.getAbsolutePath(); + } + + @Override + public int getDownloadPercent() { + if (_remoteSize == 0) { + return 0; + } + + return (int)(100.0*_totalBytes/_remoteSize); + } + + @Override + public long getDownloadTime() { + return _downloadTime; + } + + @Override + public long getDownloadedBytes() { + return _totalBytes; + } + + @Override + public Status getStatus() { + return _status; + } + + @Override + public void setDownloadError(String string) { + _errorString = string; + } + + @Override + public void setStatus(Status status) { + _status = status; + } + + @Override + public boolean stopDownload() { + switch (getStatus()) { + case IN_PROGRESS: + case UNKNOWN: + case NOT_STARTED: + case RECOVERABLE_ERROR: + case UNRECOVERABLE_ERROR: + case ABORTED: + _status = TemplateDownloader.Status.ABORTED; + break; + case DOWNLOAD_FINISHED: + break; + default: + break; + } + File f = new File(_toFile); + if (f.exists()) { + f.delete(); + } + return true; + } + + public long getMaxTemplateSizeInBytes() { + return this.MAX_TEMPLATE_SIZE_IN_BYTES; + } + + @Override + public void run() { + try { + download(_resume, _callback); + } catch (Exception e) { + s_logger.warn("Unable to complete download due to ", e); + _errorString = "Failed to install: " + e.getMessage(); + _status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; + } + } + + @Override + public void setResume(boolean resume) { + _resume = resume; + + } + + @Override + public boolean isInited() { + return _inited; + } +} diff --git a/core/src/com/cloud/storage/template/TemplateInfo.java b/core/src/com/cloud/storage/template/TemplateInfo.java new file mode 100644 index 00000000000..800417c7fa9 --- /dev/null +++ b/core/src/com/cloud/storage/template/TemplateInfo.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +public class TemplateInfo { + String templateName; + String installPath; + long size; + long id; + + boolean isPublic; + + public static TemplateInfo getDefaultSystemVmTemplateInfo() { + TemplateInfo routingInfo = new TemplateInfo(TemplateConstants.DEFAULT_SYSTEM_VM_TMPLT_NAME, TemplateConstants.DEFAULT_SYSTEM_VM_TEMPLATE_PATH, false); + return routingInfo; + } + + protected TemplateInfo() { + + } + + public TemplateInfo(String templateName, String installPath, long size, boolean isPublic) { + this.templateName = templateName; + this.installPath = installPath; + this.size = size; + this.isPublic = isPublic; + } + + public TemplateInfo(String templateName, String installPath, boolean isPublic) { + this.templateName = templateName; + this.installPath = installPath; + this.size = 0; + this.isPublic = isPublic; + } + + public long getId() { + return id; + } + + public String getTemplateName() { + return templateName; + } + + public String getInstallPath() { + return installPath; + } + + public boolean isPublic() { + return isPublic; + } + + public void setInstallPath(String installPath) { + this.installPath = installPath; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } +} diff --git a/core/src/com/cloud/storage/template/TemplateLocation.java b/core/src/com/cloud/storage/template/TemplateLocation.java new file mode 100644 index 00000000000..57785375422 --- /dev/null +++ b/core/src/com/cloud/storage/template/TemplateLocation.java @@ -0,0 +1,215 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Properties; + +import org.apache.log4j.Logger; + +import com.cloud.storage.StorageLayer; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.template.Processor.FormatInfo; +import com.cloud.utils.NumbersUtil; + +public class TemplateLocation { + private static final Logger s_logger = Logger.getLogger(TemplateLocation.class); + public final static String Filename = "template.properties"; + + StorageLayer _storage; + String _templatePath; + + File _file; + Properties _props; + + ArrayList _formats; + + public TemplateLocation(StorageLayer storage, String templatePath) { + _storage = storage; + _templatePath = templatePath; + if (!_templatePath.endsWith(File.separator)) { + _templatePath += File.separator; + } + _formats = new ArrayList(5); + _props = new Properties(); + _file = _storage.getFile(_templatePath + Filename); + } + + public boolean create(long id, boolean isPublic, String uniqueName) throws IOException { + boolean result = load(); + _props.setProperty("id", Long.toString(id)); + _props.setProperty("public", Boolean.toString(isPublic)); + _props.setProperty("uniquename", uniqueName); + + return result; + } + + public boolean purge() { + boolean purged = true; + String[] files = _storage.listFiles(_templatePath); + for (String file : files) { + boolean r = _storage.delete(file); + if (!r) { + purged = false; + } + if (s_logger.isDebugEnabled()) { + s_logger.debug((r ? "R" : "Unable to r") + "emove " + file); + } + } + + return purged; + } + + public boolean load() throws IOException { + FileInputStream strm = null; + try { + strm = new FileInputStream(_file); + _props.load(strm); + } finally { + if (strm != null) { + try { + strm.close(); + } catch (IOException e) { + } + } + } + + for (ImageFormat format : ImageFormat.values()) { + String ext = _props.getProperty(format.getFileExtension()); + if (ext != null) { + FormatInfo info = new FormatInfo(); + info.format = format; + info.filename = _props.getProperty(format.getFileExtension() + ".filename"); + info.size = NumbersUtil.parseLong(_props.getProperty(format.getFileExtension() + ".size"), -1); + info.virtualSize = NumbersUtil.parseLong(_props.getProperty(format.getFileExtension() + ".virtualsize"), -1); + + _formats.add(info); + + if (!checkFormatValidity(info)) { + s_logger.warn("Cleaning up inconsistent information for " + format); + cleanup(format); + } + } + } + + if (_props.getProperty("uniquename") == null || _props.getProperty("virtualsize") == null) { + return false; + } + + return _formats.size() > 0; + } + + public boolean save() { + for (FormatInfo info : _formats) { + _props.setProperty(info.format.getFileExtension(), "true"); + _props.setProperty(info.format.getFileExtension() + ".filename", info.filename); + _props.setProperty(info.format.getFileExtension() + ".size", Long.toString(info.size)); + _props.setProperty(info.format.getFileExtension() + ".virtualsize", Long.toString(info.virtualSize)); + } + FileOutputStream strm = null; + try { + strm = new FileOutputStream(_file); + _props.store(strm, ""); + } catch (IOException e) { + s_logger.warn("Unable to save the template properties ", e); + return false; + } finally { + if (strm != null) { + try { + strm.close(); + } catch (IOException e) { + } + } + } + + return true; + } + + public TemplateInfo getTemplateInfo() { + TemplateInfo tmplInfo = new TemplateInfo(); + + String[] tokens = _templatePath.split(File.separator); + tmplInfo.id = Long.parseLong(_props.getProperty("id")); + tmplInfo.installPath = _templatePath + File.separator + _props.getProperty("filename"); + tmplInfo.installPath = tmplInfo.installPath.substring(tmplInfo.installPath.indexOf("template")); + tmplInfo.isPublic = Boolean.parseBoolean(_props.getProperty("public")); + tmplInfo.templateName = _props.getProperty("uniquename"); + tmplInfo.size = Long.parseLong(_props.getProperty("virtualsize")); + + return tmplInfo; + } + + protected void cleanup(ImageFormat format) { + FormatInfo info = deleteFormat(format); + if (info != null && info.filename != null) { + boolean r = _storage.delete(_templatePath + info.filename); + if (s_logger.isDebugEnabled()) { + s_logger.debug((r ? "R" : "Unable to r") + "emove " + _templatePath + info.filename); + } + } + } + + public FormatInfo getFormat(ImageFormat format) { + for (FormatInfo info : _formats) { + if (info.format == format) { + return info; + } + } + + return null; + } + + public boolean addFormat(FormatInfo newInfo) { + deleteFormat(newInfo.format); + + if (!checkFormatValidity(newInfo)) { + s_logger.warn("Format is invalid "); + return false; + } + + _props.setProperty("virtualsize", Long.toString(newInfo.virtualSize)); + _formats.add(newInfo); + return true; + } + + protected boolean checkFormatValidity(FormatInfo info) { + return (info.format != null && info.size > 0 && info.virtualSize > 0 && info.filename != null && _storage.exists(_templatePath + info.filename) && _storage.getSize(_templatePath + info.filename) == info.size); + } + + protected FormatInfo deleteFormat(ImageFormat format) { + Iterator it = _formats.iterator(); + while (it.hasNext()) { + FormatInfo info = it.next(); + if (info.format == format) { + it.remove(); + _props.remove(format.getFileExtension()); + _props.remove(format.getFileExtension() + ".filename"); + _props.remove(format.getFileExtension() + ".size"); + _props.remove(format.getFileExtension() + ".virtualsize"); + return info; + } + } + + return null; + } +} diff --git a/core/src/com/cloud/storage/template/VhdProcessor.java b/core/src/com/cloud/storage/template/VhdProcessor.java new file mode 100644 index 00000000000..36ec0bf6ef8 --- /dev/null +++ b/core/src/com/cloud/storage/template/VhdProcessor.java @@ -0,0 +1,130 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.storage.template; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +import javax.naming.ConfigurationException; + +import org.apache.log4j.Logger; + +import com.cloud.exception.InternalErrorException; +import com.cloud.storage.StorageLayer; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.utils.NumbersUtil; + +/** + * VhdProcessor processes the downloaded template for VHD. It + * currently does not handle any type of template conversion + * into the VHD format. + * + */ +public class VhdProcessor implements Processor { + + private static final Logger s_logger = Logger.getLogger(VhdProcessor.class); + String _name; + StorageLayer _storage; + private int vhd_footer_size = 512; + private int vhd_footer_creator_app_offset = 28; + private int vhd_footer_creator_ver_offset = 32; + private int vhd_footer_current_size_offset = 48; + private byte[] citrix_creator_app = {0x74, 0x61, 0x70, 0x00}; /*"tap "*/ + + @Override + public FormatInfo process(String templatePath, ImageFormat format, String templateName) throws InternalErrorException { + if (format != null) { + s_logger.debug("We currently don't handle conversion from " + format + " to VHD."); + return null; + } + + String vhdPath = templatePath + File.separator + templateName + "." + ImageFormat.VHD.getFileExtension(); + + if (!_storage.exists(vhdPath)) { + s_logger.debug("Unable to find the vhd file: " + vhdPath); + return null; + } + + FormatInfo info = new FormatInfo(); + info.format = ImageFormat.VHD; + info.filename = templateName + "." + ImageFormat.VHD.getFileExtension(); + + File vhdFile = _storage.getFile(vhdPath); + + info.size = _storage.getSize(vhdPath); + FileInputStream strm = null; + byte[] currentSize = new byte[8]; + byte[] creatorApp = new byte[4]; + try { + strm = new FileInputStream(vhdFile); + strm.skip(info.size - vhd_footer_size + vhd_footer_creator_app_offset); + strm.read(creatorApp); + strm.skip(vhd_footer_current_size_offset - vhd_footer_creator_ver_offset); + strm.read(currentSize); + } catch (Exception e) { + s_logger.warn("Unable to read vhd file " + vhdPath, e); + throw new InternalErrorException("Unable to read vhd file " + vhdPath + ": " + e); + } finally { + if (strm != null) { + try { + strm.close(); + } catch (IOException e) { + } + } + } + + if (!Arrays.equals(creatorApp, citrix_creator_app)) { + /*Only support VHD image created by citrix xenserver*/ + throw new InternalErrorException("Image creator is:" + creatorApp.toString() +", is not supported"); + } + + long templateSize = NumbersUtil.bytesToLong(currentSize); + info.virtualSize = templateSize; + + return info; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + _name = name; + _storage = (StorageLayer)params.get(StorageLayer.InstanceConfigKey); + if (_storage == null) { + throw new ConfigurationException("Unable to get storage implementation"); + } + + return true; + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } +} diff --git a/core/src/com/cloud/user/Account.java b/core/src/com/cloud/user/Account.java new file mode 100644 index 00000000000..bb419b1007a --- /dev/null +++ b/core/src/com/cloud/user/Account.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user; + +import java.util.Date; + +public interface Account { + public static final short ACCOUNT_TYPE_NORMAL = 0; + public static final short ACCOUNT_TYPE_ADMIN = 1; + public static final short ACCOUNT_TYPE_DOMAIN_ADMIN = 2; + public static final short ACCOUNT_TYPE_READ_ONLY_ADMIN = 3; + + public static final String ACCOUNT_STATE_DISABLED = "disabled"; + public static final String ACCOUNT_STATE_ENABLED = "enabled"; + public static final String ACCOUNT_STATE_LOCKED = "locked"; + + public static final long ACCOUNT_ID_SYSTEM = 1; + + public Long getId(); + public String getAccountName(); + public void setAccountName(String accountId); + public short getType(); + public void setType(short type); + public String getState(); + public void setState(String state); + public Long getDomainId(); + public void setDomainId(Long domainId); + public Date getRemoved(); +} diff --git a/core/src/com/cloud/user/AccountVO.java b/core/src/com/cloud/user/AccountVO.java new file mode 100644 index 00000000000..42c30ee7ca3 --- /dev/null +++ b/core/src/com/cloud/user/AccountVO.java @@ -0,0 +1,105 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="account") +public class AccountVO implements Account { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="account_name") + private String accountName = null; + + @Column(name="type") + private short type = ACCOUNT_TYPE_NORMAL; + + @Column(name="domain_id") + private Long domainId = null; + + @Column(name="state") + private String state; + + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + @Column(name="cleanup_needed") + private boolean needsCleanup = false; + + public AccountVO() {} + public AccountVO(Long id) { + this.id = id; + } + + public void setNeedsCleanup(boolean value) { + needsCleanup = value; + } + + public boolean getNeedsCleanup() { + return needsCleanup; + } + + public Long getId() { + return id; + } + + public String getAccountName() { + return accountName; + } + public void setAccountName(String accountName) { + this.accountName = accountName; + } + public short getType() { + return type; + } + public void setType(short type) { + this.type = type; + } + + public Long getDomainId() { + return domainId; + } + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public String getState() { + return state; + } + public void setState(String state) { + this.state = state; + } + + public Date getRemoved() { + return removed; + } +} diff --git a/core/src/com/cloud/user/User.java b/core/src/com/cloud/user/User.java new file mode 100644 index 00000000000..f5e5adc34c3 --- /dev/null +++ b/core/src/com/cloud/user/User.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user; + +import java.util.Date; + +public interface User { + public static final long UID_SYSTEM = 1; + + public Long getId(); + + public Date getCreated(); + + public Date getRemoved(); + + public String getUsername(); + + public void setUsername(String username); + + public String getPassword(); + + public void setPassword(String password); + + public String getFirstname(); + + public void setFirstname(String firstname); + + public String getLastname(); + + public void setLastname(String lastname); + + public long getAccountId(); + + public void setAccountId(long accountId); + + public String getEmail(); + + public void setEmail(String email); + + public String getState(); + + public void setState(String state); + + public String getApiKey(); + + public void setApiKey(String apiKey); + + public String getSecretKey(); + + public void setSecretKey(String secretKey); + + public String getTimezone(); + + public void setTimezone(String timezone); + +} \ No newline at end of file diff --git a/core/src/com/cloud/user/UserAccount.java b/core/src/com/cloud/user/UserAccount.java new file mode 100644 index 00000000000..c40b92d8ef2 --- /dev/null +++ b/core/src/com/cloud/user/UserAccount.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user; + +import java.util.Date; + +public interface UserAccount { + Long getId(); + + String getUsername(); + + String getPassword(); + + String getFirstname(); + + String getLastname(); + + long getAccountId(); + + String getEmail(); + + String getState(); + + String getApiKey(); + + String getSecretKey(); + + Date getCreated(); + + Date getRemoved(); + + String getAccountName(); + + short getType(); + + Long getDomainId(); + + String getAccountState(); + + String getTimezone(); +} diff --git a/core/src/com/cloud/user/UserAccountVO.java b/core/src/com/cloud/user/UserAccountVO.java new file mode 100644 index 00000000000..7442d6d96de --- /dev/null +++ b/core/src/com/cloud/user/UserAccountVO.java @@ -0,0 +1,247 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.SecondaryTable; +import javax.persistence.Table; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="user") +@SecondaryTable(name="account", + pkJoinColumns={@PrimaryKeyJoinColumn(name="account_id", referencedColumnName="id")}) +public class UserAccountVO implements UserAccount { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="username") + private String username = null; + + @Column(name="password") + private String password = null; + + @Column(name="firstname") + private String firstname = null; + + @Column(name="lastname") + private String lastname = null; + + @Column(name="account_id") + private long accountId; + + @Column(name="email") + private String email = null; + + @Column(name="state") + private String state; + + @Column(name="api_key") + private String apiKey = null; + + @Column(name="secret_key") + private String secretKey = null; + + @Column(name=GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + @Column(name="timezone") + private String timezone; + + @Column(name="account_name", table="account", insertable=false, updatable=false) + private String accountName = null; + + @Column(name="type", table="account", insertable=false, updatable=false) + private short type; + + @Column(name="domain_id", table="account", insertable=false, updatable=false) + private Long domainId = null; + + @Column(name="state", table="account", insertable=false, updatable=false) + private String accountState; + + public UserAccountVO() {} + + @Override + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + @Override + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + @Override + public long getAccountId() { + return accountId; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + @Override + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Override + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + @Override + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + @Override + public Date getCreated() { + return created; + } + +// public void setCreated(Date created) { +// this.created = created; +// } + + @Override + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + @Override + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + @Override + public short getType() { + return type; + } + + public void setType(short type) { + this.type = type; + } + + @Override + public Long getDomainId() { + return domainId; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + @Override + public String getAccountState() { + return accountState; + } + + public void setAccountDisabled(String accountState) { + this.accountState = accountState; + } + + @Override + public String getTimezone(){ + return timezone; + } + + public void setTimezone(String timezone) + { + this.timezone = timezone; + } +} \ No newline at end of file diff --git a/core/src/com/cloud/user/UserContext.java b/core/src/com/cloud/user/UserContext.java new file mode 100644 index 00000000000..36064b923b6 --- /dev/null +++ b/core/src/com/cloud/user/UserContext.java @@ -0,0 +1,115 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user; + +import org.apache.log4j.Logger; + +import com.cloud.utils.ProcessUtil; + + +public class UserContext { + private static final Logger s_logger = Logger.getLogger(UserContext.class); + + private static ThreadLocal s_currentContext = new ThreadLocal(); + + private Long userId; + private Long accountId; + private String sessionId; + + private boolean apiServer; + + private static UserContext s_nullContext = new UserContext(); + + public UserContext() { + } + + public UserContext(Long userId, Long accountId, String sessionId, boolean apiServer) { + this.userId = userId; + this.accountId = accountId; + this.sessionId = sessionId; + this.apiServer = apiServer; + } + + public long getUserId() { + if(userId != null) + return userId.longValue(); + + if(!apiServer) + s_logger.warn("Null user id in UserContext " + ProcessUtil.dumpStack()); + + return 0; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public long getAccountId() { + if(accountId != null) + return accountId.longValue(); + + if(!apiServer) + s_logger.warn("Null account id in UserContext " + ProcessUtil.dumpStack()); + return 0; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionKey(String sessionId) { + this.sessionId = sessionId; + } + + public boolean isApiServer() { + return apiServer; + } + + public void setApiServer(boolean apiServer) { + this.apiServer = apiServer; + } + + public static UserContext current() { + UserContext context = s_currentContext.get(); + if(context == null) + return s_nullContext; + return context; + } + + public static void updateContext(Long userId, Long accountId, String sessionId) { + UserContext context = current(); + assert(context != null) : "Context should be already setup before you can call this one"; + + context.setUserId(userId); + context.setAccountId(accountId); + context.setSessionKey(sessionId); + } + + public static void registerContext(Long userId, Long accountId, String sessionId, boolean apiServer) { + s_currentContext.set(new UserContext(userId, accountId, sessionId, apiServer)); + } + + public static void unregisterContext() { + s_currentContext.set(null); + } +} diff --git a/core/src/com/cloud/user/UserStatisticsVO.java b/core/src/com/cloud/user/UserStatisticsVO.java new file mode 100644 index 00000000000..8f1cb26e161 --- /dev/null +++ b/core/src/com/cloud/user/UserStatisticsVO.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="user_statistics") +public class UserStatisticsVO { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id; + + @Column(name="data_center_id", updatable=false) + private long dataCenterId; + + @Column(name="account_id", updatable=false) + private long accountId; + + @Column(name="net_bytes_received") + private long netBytesReceived; + + @Column(name="net_bytes_sent") + private long netBytesSent; + + @Column(name="current_bytes_received") + private long currentBytesReceived; + + @Column(name="current_bytes_sent") + private long currentBytesSent; + + protected UserStatisticsVO() { + } + + public UserStatisticsVO(long accountId, long dcId) { + this.accountId = accountId; + this.netBytesReceived = 0; + this.netBytesSent = 0; + currentBytesReceived = 0; + currentBytesSent = 0; + dataCenterId = dcId; + } + + public long getAccountId() { + return accountId; + } + + public Long getId() { + return id; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public long getCurrentBytesReceived() { + return currentBytesReceived; + } + + public void setCurrentBytesReceived(long currentBytesReceived) { + this.currentBytesReceived = currentBytesReceived; + } + + public long getCurrentBytesSent() { + return currentBytesSent; + } + + public void setCurrentBytesSent(long currentBytesSent) { + this.currentBytesSent = currentBytesSent; + } + + public long getNetBytesReceived() { + return netBytesReceived; + } + + public long getNetBytesSent() { + return netBytesSent; + } + + public void setNetBytesReceived(long netBytesReceived) { + this.netBytesReceived = netBytesReceived; + } + + public void setNetBytesSent(long netBytesSent) { + this.netBytesSent = netBytesSent; + } +} diff --git a/core/src/com/cloud/user/UserVO.java b/core/src/com/cloud/user/UserVO.java new file mode 100644 index 00000000000..2c04e9f48d4 --- /dev/null +++ b/core/src/com/cloud/user/UserVO.java @@ -0,0 +1,160 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.utils.db.GenericDao; + +/** + * A bean representing a user + * + * @author Will Chan + * + */ +@Entity +@Table(name="user") +public class UserVO implements User { + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name="id") + private Long id = null; + + @Column(name="username") + private String username = null; + + @Column(name="password") + private String password = null; + + @Column(name="firstname") + private String firstname = null; + + @Column(name="lastname") + private String lastname = null; + + @Column(name="account_id") + private long accountId; + + @Column(name="email") + private String email = null; + + @Column(name="state") + private String state; + + @Column(name="api_key") + private String apiKey = null; + + @Column(name="secret_key") + private String secretKey = null; + + @Column(name=GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name=GenericDao.REMOVED_COLUMN) + private Date removed; + + @Column(name="timezone") + private String timezone; + + public UserVO() {} + public UserVO(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public Date getCreated() { + return created; + } + public Date getRemoved() { + return removed; + } + + public String getUsername() { + return username; + } + public void setUsername(String username) { + this.username = username; + } + public String getPassword() { + return password; + } + public void setPassword(String password) { + this.password = password; + } + public String getFirstname() { + return firstname; + } + public void setFirstname(String firstname) { + this.firstname = firstname; + } + public String getLastname() { + return lastname; + } + public void setLastname(String lastname) { + this.lastname = lastname; + } + public long getAccountId() { + return accountId; + } + public void setAccountId(long accountId) { + this.accountId = accountId; + } + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } + public String getState() { + return state; + } + public void setState(String state) { + this.state = state; + } + public String getApiKey() { + return apiKey; + } + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + public String getSecretKey() { + return secretKey; + } + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + public String getTimezone() + { + return timezone; + } + public void setTimezone(String timezone) + { + this.timezone = timezone; + } +} diff --git a/core/src/com/cloud/user/dao/AccountDao.java b/core/src/com/cloud/user/dao/AccountDao.java new file mode 100644 index 00000000000..895b74ba94c --- /dev/null +++ b/core/src/com/cloud/user/dao/AccountDao.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.user.Account; +import com.cloud.user.AccountVO; +import com.cloud.user.User; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDao; + +public interface AccountDao extends GenericDao { + Pair findUserAccountByApiKey(String apiKey); + List findAccountsLike(String accountName); + Account findActiveAccount(String accountName, Long domainId); + Account findActiveAccountByName(String accountName); + Account findAccount(String accountName, Long domainId); + List findActiveAccounts(Long maxAccountId, Filter filter); + List findRecentlyDeletedAccounts(Long maxAccountId, Date earliestRemovedDate, Filter filter); + List findNewAccounts(Long minAccountId, Filter filter); + List findCleanups(); + List findAdminAccountsForDomain(Long domainId); + void markForCleanup(long accountId); +} diff --git a/core/src/com/cloud/user/dao/AccountDaoImpl.java b/core/src/com/cloud/user/dao/AccountDaoImpl.java new file mode 100644 index 00000000000..c9dd9c0b401 --- /dev/null +++ b/core/src/com/cloud/user/dao/AccountDaoImpl.java @@ -0,0 +1,183 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.user.Account; +import com.cloud.user.AccountVO; +import com.cloud.user.User; +import com.cloud.user.UserVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.SearchCriteria.Op; + +@Local(value={AccountDao.class}) +public class AccountDaoImpl extends GenericDaoBase implements AccountDao { + private static final Logger s_logger = Logger.getLogger(AccountDaoImpl.class); + private final String FIND_USER_ACCOUNT_BY_API_KEY = "SELECT u.id, u.username, u.account_id, u.secret_key, u.state, " + + "a.id, a.account_name, a.type, a.domain_id, a.state " + + "FROM `cloud`.`user` u, `cloud`.`account` a " + + "WHERE u.account_id = a.id AND u.api_key = ? and u.removed IS NULL"; + + protected final SearchBuilder AccountNameSearch; + protected final SearchBuilder AccountTypeSearch; + + protected final SearchBuilder CleanupSearch; + + protected AccountDaoImpl() { + AccountNameSearch = createSearchBuilder(); + AccountNameSearch.and("accountName", AccountNameSearch.entity().getAccountName(), SearchCriteria.Op.EQ); + AccountNameSearch.done(); + + AccountTypeSearch = createSearchBuilder(); + AccountTypeSearch.and("domainId", AccountTypeSearch.entity().getDomainId(), SearchCriteria.Op.EQ); + AccountTypeSearch.and("type", AccountTypeSearch.entity().getType(), SearchCriteria.Op.EQ); + AccountTypeSearch.done(); + + CleanupSearch = createSearchBuilder(); + CleanupSearch.and("cleanup", CleanupSearch.entity().getNeedsCleanup(), SearchCriteria.Op.EQ); + CleanupSearch.and("removed", CleanupSearch.entity().getRemoved(), SearchCriteria.Op.NNULL); + CleanupSearch.done(); + + } + + @Override + public List findCleanups() { + SearchCriteria sc = CleanupSearch.create(); + sc.setParameters("cleanup", true); + + return searchAll(sc, null, null, false); + } + + public Pair findUserAccountByApiKey(String apiKey) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + Pair userAcctPair = null; + try { + String sql = FIND_USER_ACCOUNT_BY_API_KEY; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setString(1, apiKey); + ResultSet rs = pstmt.executeQuery(); + // TODO: make sure we don't have more than 1 result? ApiKey had better be unique + if (rs.next()) { + User u = new UserVO(rs.getLong(1)); + u.setUsername(rs.getString(2)); + u.setAccountId(rs.getLong(3)); + u.setSecretKey(rs.getString(4)); + u.setState(rs.getString(5)); + + Account a = new AccountVO(rs.getLong(6)); + a.setAccountName(rs.getString(7)); + a.setType(rs.getShort(8)); + a.setDomainId(rs.getLong(9)); + a.setState(rs.getString(10)); + + userAcctPair = new Pair(u, a); + } + } catch (Exception e) { + s_logger.warn("Exception finding user/acct by api key: " + apiKey, e); + } + return userAcctPair; + } + + @Override + public List findAccountsLike(String accountName) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("accountName", SearchCriteria.Op.LIKE, "%"+accountName+"%"); + return listActiveBy(sc); + } + + @Override + public Account findActiveAccount(String accountName, Long domainId) { + SearchCriteria sc = AccountNameSearch.create("accountName", accountName); + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + return findOneActiveBy(sc); + } + + @Override + public Account findAccount(String accountName, Long domainId) { + SearchCriteria sc = AccountNameSearch.create("accountName", accountName); + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + return findOneBy(sc); + } + + public Account findActiveAccountByName(String accountName) { + SearchCriteria sc = AccountNameSearch.create("accountName", accountName); + return findOneActiveBy(sc); + } + + public List findActiveAccounts(Long maxAccountId, Filter filter) { + if (maxAccountId == null) return null; + + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("id", SearchCriteria.Op.LTEQ, maxAccountId); + + return listActiveBy(sc, filter); + } + + public List findRecentlyDeletedAccounts(Long maxAccountId, Date earliestRemovedDate, Filter filter) { + if (earliestRemovedDate == null) return null; + SearchCriteria sc = createSearchCriteria(); + if (maxAccountId != null) { + sc.addAnd("id", SearchCriteria.Op.LTEQ, maxAccountId); + } + sc.addAnd("removed", SearchCriteria.Op.NNULL); + sc.addAnd("removed", SearchCriteria.Op.GTEQ, earliestRemovedDate); + + return listBy(sc, filter); + } + + public List findNewAccounts(Long minAccountId, Filter filter) { + if (minAccountId == null) return null; + + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("id", SearchCriteria.Op.GT, minAccountId); + + return listBy(sc, filter); + } + + @Override + public List findAdminAccountsForDomain(Long domain) { + SearchCriteria sc = AccountTypeSearch.create(); + sc.addAnd("domainId", Op.EQ, domain); + sc.addAnd("type", Op.IN, Account.ACCOUNT_TYPE_ADMIN, Account.ACCOUNT_TYPE_DOMAIN_ADMIN, Account.ACCOUNT_TYPE_READ_ONLY_ADMIN); + return null; + } + + @Override + public void markForCleanup(long accountId) { + AccountVO account = findById(accountId); + if (!account.getNeedsCleanup()) { + account.setNeedsCleanup(true); + update(accountId, account); + } + } +} diff --git a/core/src/com/cloud/user/dao/UserAccountDao.java b/core/src/com/cloud/user/dao/UserAccountDao.java new file mode 100644 index 00000000000..adb38c6a438 --- /dev/null +++ b/core/src/com/cloud/user/dao/UserAccountDao.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user.dao; + +import com.cloud.user.UserAccount; +import com.cloud.user.UserAccountVO; +import com.cloud.utils.db.GenericDao; + +public interface UserAccountDao extends GenericDao { + UserAccount getUserAccount(String username, Long domainId); + boolean validateUsernameInDomain(String username, Long domainId); +} diff --git a/core/src/com/cloud/user/dao/UserAccountDaoImpl.java b/core/src/com/cloud/user/dao/UserAccountDaoImpl.java new file mode 100644 index 00000000000..175f5ca3c7e --- /dev/null +++ b/core/src/com/cloud/user/dao/UserAccountDaoImpl.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user.dao; + +import javax.ejb.Local; + +import com.cloud.user.UserAccount; +import com.cloud.user.UserAccountVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchCriteria; + +@Local(value={UserAccountDao.class}) +public class UserAccountDaoImpl extends GenericDaoBase implements UserAccountDao { + @Override + public UserAccount getUserAccount(String username, Long domainId) { + if ((username == null) || (domainId == null)) { + return null; + } + + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("username", SearchCriteria.Op.EQ, username); + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + return findOneActiveBy(sc); + } + + @Override + public boolean validateUsernameInDomain(String username, Long domainId) { + UserAccount userAcct = getUserAccount(username, domainId); + if (userAcct == null) { + return true; + } + return false; + } +} diff --git a/core/src/com/cloud/user/dao/UserDao.java b/core/src/com/cloud/user/dao/UserDao.java new file mode 100644 index 00000000000..436a830b353 --- /dev/null +++ b/core/src/com/cloud/user/dao/UserDao.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user.dao; + +import java.util.List; + +import com.cloud.user.UserVO; +import com.cloud.utils.db.GenericDao; + + +/* + * Data Access Object for user table + */ +public interface UserDao extends GenericDao{ + UserVO getUser(String username, String password); + UserVO getUser(String username); + UserVO getUser(long userId); + List findUsersLike(String username); + + /** + * updates a user with the new username, password, firstname, lastname, email,accountId, timezone + * @param id + * @param username + * @param password + * @param firstname + * @param lastname + * @param email + * @param accountId + * @param timezone + * @param apikey + * @param secretkey + */ + void update(long id, String username, String password, String firstname, String lastname, String email, Long accountId, String timezone, String apiKey, String secretKey); + + List listByAccount(long accountId); + + /** + * Finds a user based on the secret key provided. + * @param secretKey + * @return + */ + UserVO findUserBySecretKey(String secretKey); +} diff --git a/core/src/com/cloud/user/dao/UserDaoImpl.java b/core/src/com/cloud/user/dao/UserDaoImpl.java new file mode 100644 index 00000000000..7fefd2e3c24 --- /dev/null +++ b/core/src/com/cloud/user/dao/UserDaoImpl.java @@ -0,0 +1,137 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user.dao; + +import java.util.List; + +import javax.ejb.Local; + +import com.cloud.user.UserVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.exception.CloudRuntimeException; + +/** + * Implementation of the UserDao + * + * @author Will Chan + * + */ +@Local(value={UserDao.class}) +public class UserDaoImpl extends GenericDaoBase implements UserDao { + protected SearchBuilder UsernamePasswordSearch; + protected SearchBuilder UsernameSearch; + protected SearchBuilder UsernameLikeSearch; + protected SearchBuilder UserIdSearch; + protected SearchBuilder AccountIdSearch; + protected SearchBuilder SecretKeySearch; + + protected UserDaoImpl () { + UsernameSearch = createSearchBuilder(); + UsernameSearch.and("username", UsernameSearch.entity().getUsername(), SearchCriteria.Op.EQ); + UsernameSearch.done(); + + UsernameLikeSearch = createSearchBuilder(); + UsernameLikeSearch.and("username", UsernameLikeSearch.entity().getUsername(), SearchCriteria.Op.LIKE); + UsernameLikeSearch.done(); + + AccountIdSearch = createSearchBuilder(); + AccountIdSearch.and("account", AccountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountIdSearch.done(); + + UsernamePasswordSearch = createSearchBuilder(); + UsernamePasswordSearch.and("username", UsernamePasswordSearch.entity().getUsername(), SearchCriteria.Op.EQ); + UsernamePasswordSearch.and("password", UsernamePasswordSearch.entity().getPassword(), SearchCriteria.Op.EQ); + UsernamePasswordSearch.done(); + + UserIdSearch = createSearchBuilder(); + UserIdSearch.and("id", UserIdSearch.entity().getId(), SearchCriteria.Op.EQ); + UserIdSearch.done(); + + SecretKeySearch = createSearchBuilder(); + SecretKeySearch.and("secretKey", SecretKeySearch.entity().getSecretKey(), SearchCriteria.Op.EQ); + SecretKeySearch.done(); + } + + @Override + public UserVO getUser(String username, String password) { + SearchCriteria sc = UsernamePasswordSearch.create(); + sc.setParameters("username", username); + sc.setParameters("password", password); + return findOneActiveBy(sc); + } + + public List listByAccount(long accountId) { + SearchCriteria sc = AccountIdSearch.create(); + sc.setParameters("account", accountId); + return listActiveBy(sc, null); + } + + @Override + public UserVO getUser(String username) { + SearchCriteria sc = UsernameSearch.create(); + sc.setParameters("username", username); + return findOneActiveBy(sc); + } + + @Override + public UserVO getUser(long userId) { + SearchCriteria sc = UserIdSearch.create(); + sc.setParameters("id", userId); + return findOneActiveBy(sc); + } + + @Override + public List findUsersLike(String username) { + SearchCriteria sc = UsernameLikeSearch.create(); + sc.setParameters("username", "%" + username + "%"); + return listActiveBy(sc); + } + + @Override + public UserVO findUserBySecretKey(String secretKey) { + SearchCriteria sc = SecretKeySearch.create(); + sc.setParameters("secretKey", secretKey); + return findOneActiveBy(sc); + } + + @Override + public void update(long id, String username, String password, String firstname, String lastname, String email, Long accountId, String timezone, String apiKey, String secretKey) + { + UserVO dbUser = getUser(username); + if ((dbUser == null) || (dbUser.getId().longValue() == id)) { + UserVO ub = createForUpdate(); + ub.setUsername(username); + ub.setPassword(password); + ub.setFirstname(firstname); + ub.setLastname(lastname); + ub.setEmail(email); + ub.setAccountId(accountId); + ub.setTimezone(timezone); + ub.setApiKey(apiKey); + ub.setSecretKey(secretKey); + update(id, ub); + } + else + { + throw new CloudRuntimeException("unable to update user -- a user with that name exists"); + } + } +} diff --git a/core/src/com/cloud/user/dao/UserStatisticsDao.java b/core/src/com/cloud/user/dao/UserStatisticsDao.java new file mode 100644 index 00000000000..7f9e06ee8a7 --- /dev/null +++ b/core/src/com/cloud/user/dao/UserStatisticsDao.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.user.UserStatisticsVO; +import com.cloud.utils.db.GenericDao; + +public interface UserStatisticsDao extends GenericDao { + UserStatisticsVO findBy(long accountId, long dcId); + + UserStatisticsVO lock(long accountId, long dcId); + + List listBy(long accountId); + + List listActiveAndRecentlyDeleted(Date minRemovedDate, int startIndex, int limit); +} diff --git a/core/src/com/cloud/user/dao/UserStatisticsDaoImpl.java b/core/src/com/cloud/user/dao/UserStatisticsDaoImpl.java new file mode 100644 index 00000000000..72298ba8848 --- /dev/null +++ b/core/src/com/cloud/user/dao/UserStatisticsDaoImpl.java @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.user.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.user.UserStatisticsVO; +import com.cloud.utils.DateUtil; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; + +@Local(value={UserStatisticsDao.class}) +public class UserStatisticsDaoImpl extends GenericDaoBase implements UserStatisticsDao { + private static final Logger s_logger = Logger.getLogger(UserStatisticsDaoImpl.class); + private static final String ACTIVE_AND_RECENTLY_DELETED_SEARCH = "SELECT us.id, us.data_center_id, us.account_id, us.net_bytes_received, us.net_bytes_sent, us.current_bytes_received, us.current_bytes_sent " + + "FROM user_statistics us, account a " + + "WHERE us.account_id = a.id AND (a.removed IS NULL OR a.removed >= ?) " + + "ORDER BY us.id"; + private final SearchBuilder UserDcSearch; + private final SearchBuilder UserSearch; + + public UserStatisticsDaoImpl() { + UserSearch = createSearchBuilder(); + UserSearch.and("account", UserSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + UserSearch.done(); + + UserDcSearch = createSearchBuilder(); + UserDcSearch.and("account", UserDcSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + UserDcSearch.and("dc", UserDcSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + UserDcSearch.done(); + } + + @Override + public UserStatisticsVO findBy(long accountId, long dcId) { + SearchCriteria sc = UserDcSearch.create(); + sc.setParameters("account", accountId); + sc.setParameters("dc", dcId); + return findOneActiveBy(sc); + } + + @Override + public UserStatisticsVO lock(long accountId, long dcId) { + SearchCriteria sc = UserDcSearch.create(); + sc.setParameters("account", accountId); + sc.setParameters("dc", dcId); + return lock(sc, true); + } + + @Override + public List listBy(long accountId) { + SearchCriteria sc = UserSearch.create(); + sc.setParameters("account", accountId); + return search(sc, null); + } + + @Override + public List listActiveAndRecentlyDeleted(Date minRemovedDate, int startIndex, int limit) { + List userStats = new ArrayList(); + if (minRemovedDate == null) return userStats; + + Transaction txn = Transaction.currentTxn(); + try { + String sql = ACTIVE_AND_RECENTLY_DELETED_SEARCH + " LIMIT " + startIndex + "," + limit; + PreparedStatement pstmt = null; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setString(1, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), minRemovedDate)); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + userStats.add(toEntityBean(rs, false)); + } + } catch (Exception ex) { + s_logger.error("error saving user stats to cloud_usage db", ex); + } + return userStats; + } +} diff --git a/core/src/com/cloud/vm/ConsoleProxy.java b/core/src/com/cloud/vm/ConsoleProxy.java new file mode 100644 index 00000000000..66d2da16eb2 --- /dev/null +++ b/core/src/com/cloud/vm/ConsoleProxy.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.vm; + +import java.util.Date; + +/** + * ConsoleProxy is a system VM instance that is used + * to proxy VNC traffic + */ +public interface ConsoleProxy extends VirtualMachine { + + public String getGateway(); + public String getDns1(); + public String getDns2(); + public String getDomain(); + public String getPublicIpAddress(); + public String getPublicNetmask(); + public String getPublicMacAddress(); + public Long getVlanDbId(); + public String getVlanId(); + public String getPrivateNetmask(); + public int getRamSize(); + public int getActiveSession(); + public Date getLastUpdateTime(); + public byte[] getSessionDetails(); +} + diff --git a/core/src/com/cloud/vm/ConsoleProxyVO.java b/core/src/com/cloud/vm/ConsoleProxyVO.java new file mode 100644 index 00000000000..20499b437b8 --- /dev/null +++ b/core/src/com/cloud/vm/ConsoleProxyVO.java @@ -0,0 +1,296 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.vm; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.Transient; + +/** + * ConsoleProxyVO domain object + */ + +@Entity +@Table(name="console_proxy") +@PrimaryKeyJoinColumn(name="id") +@DiscriminatorValue(value="ConsoleProxy") +public class ConsoleProxyVO extends VMInstanceVO implements ConsoleProxy { + + @Column(name="gateway", nullable=false) + private String gateway; + + @Column(name="dns1") + private String dns1; + + @Column(name="dns2") + private String dns2; + + @Column(name="guest_mac_address") + private String guestMacAddress; + + @Column(name="guest_ip_address") + private String guestIpAddress; + + @Column(name="guest_netmask") + private String guestNetmask; + + @Column(name="public_ip_address", nullable=false) + private String publicIpAddress; + + @Column(name="public_mac_address", nullable=false) + private String publicMacAddress; + + @Column(name="public_netmask", nullable=false) + private String publicNetmask; + + @Column(name="vlan_db_id") + private Long vlanDbId; + + @Column(name="vlan_id") + private String vlanId; + + @Column(name="domain", nullable=false) + private String domain; + + @Column(name="ram_size", updatable=false, nullable=false) + private int ramSize; + + @Column(name="active_session", updatable=true, nullable=false) + private int activeSession; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name="last_update", updatable=true, nullable=true) + private Date lastUpdateTime; + + @Column(name="session_details", updatable=true, nullable=true) + private byte[] sessionDetails; + + @Transient + private boolean sslEnabled = false; + + @Transient + private int port; + + public ConsoleProxyVO( + long id, + String name, + String guestMacAddress, + String guestIpAddress, + String guestNetMask, + String privateMacAddress, + String privateIpAddress, + String privateNetmask, + long templateId, + long guestOSId, + String publicMacAddress, + String publicIpAddress, + String publicNetmask, + Long vlanDbId, + String vlanId, + long podId, + long dataCenterId, + String gateway, + Long hostId, + String dns1, + String dns2, + String domain, + int ramSize, + int activeSession) { + super(id, name, name, Type.ConsoleProxy, templateId, guestOSId, + privateMacAddress, privateIpAddress, privateNetmask, dataCenterId, podId, true, hostId); + this.gateway = gateway; + this.publicIpAddress = publicIpAddress; + this.publicNetmask = publicNetmask; + this.publicMacAddress = publicMacAddress; + this.guestIpAddress = guestIpAddress; + this.guestMacAddress = guestMacAddress; + this.guestNetmask = guestNetMask; + this.vlanDbId = vlanDbId; + this.vlanId = vlanId; + this.dns1 = dns1; + this.dns2 = dns2; + this.domain = domain; + this.ramSize = ramSize; + this.activeSession = activeSession; + } + + protected ConsoleProxyVO() { + super(); + } + + public void setGateway(String gateway) { + this.gateway = gateway; + } + + public void setDns1(String dns1) { + this.dns1 = dns1; + } + + public void setDns2(String dns2) { + this.dns2 = dns2; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public void setPublicIpAddress(String publicIpAddress) { + this.publicIpAddress = publicIpAddress; + } + + public void setPublicNetmask(String publicNetmask) { + this.publicNetmask = publicNetmask; + } + + public void setPublicMacAddress(String publicMacAddress) { + this.publicMacAddress = publicMacAddress; + } + + public void setGuestIpAddress(String guestIpAddress) { + this.guestIpAddress = guestIpAddress; + } + + public void setGuestNetmask(String guestNetmask) { + this.guestNetmask = guestNetmask; + } + + public void setGuestMacAddress(String guestMacAddress) { + this.guestMacAddress = guestMacAddress; + } + + public void setRamSize(int ramSize) { + this.ramSize = ramSize; + } + + public void setActiveSession(int activeSession) { + this.activeSession = activeSession; + } + + public void setLastUpdateTime(Date time) { + this.lastUpdateTime = time; + } + + public void setSessionDetails(byte[] details) { + this.sessionDetails = details; + } + + @Override + public String getGateway() { + return this.gateway; + } + + @Override + public String getDns1() { + return this.dns1; + } + + @Override + public String getDns2() { + return this.dns2; + } + + @Override + public String getPublicIpAddress() { + return this.publicIpAddress; + } + + @Override + public String getPublicNetmask() { + return this.publicNetmask; + } + + @Override + public String getPublicMacAddress() { + return this.publicMacAddress; + } + + + public String getGuestIpAddress() { + return this.guestIpAddress; + } + + + public String getGuestNetmask() { + return this.guestNetmask; + } + + + public String getGuestMacAddress() { + return this.guestMacAddress; + } + + @Override + public Long getVlanDbId() { + return vlanDbId; + } + + @Override + public String getVlanId() { + return vlanId; + } + + @Override + public String getDomain() { + return this.domain; + } + + @Override + public int getRamSize() { + return this.ramSize; + } + + @Override + public int getActiveSession() { + return this.activeSession; + } + + @Override + public Date getLastUpdateTime() { + return this.lastUpdateTime; + } + + @Override + public byte[] getSessionDetails() { + return this.sessionDetails; + } + + public boolean isSslEnabled() { + return sslEnabled; + } + + public void setSslEnabled(boolean sslEnabled) { + this.sslEnabled = sslEnabled; + } + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return port; + } + +} diff --git a/core/src/com/cloud/vm/DomainRouter.java b/core/src/com/cloud/vm/DomainRouter.java new file mode 100755 index 00000000000..43bcd483568 --- /dev/null +++ b/core/src/com/cloud/vm/DomainRouter.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +/** + * VirtualMachineRouter is a small VM instance that is started to + * bridge internal and external traffic. + */ +public interface DomainRouter extends VirtualMachine { + public enum Role { + DHCP_FIREWALL_LB_PASSWD_USERDATA, + DHCP_USERDATA + } + /** + * @return the mac address for the router. + */ + public String getGuestMacAddress(); + + public String getGuestIpAddress(); + + public String getPublicMacAddress(); + + public String getPublicNetmask(); + + public String getPrivateNetmask(); + + public String getVnet(); + + public String getVlanId(); + + public String getZoneVlan(); + + public String getGuestZoneMacAddress(); + + /** + * @return the gateway address for the router to use. + */ + public String getGateway(); + + /** + * @return the ram size for this machine. + */ + public int getRamSize(); + + public String getGuestNetmask(); + + /** + * @return the public ip address used for source nat. + */ + String getPublicIpAddress(); + + String getDns1(); + String getDns2(); + String getDomain(); + + /** + * @return account id that the domain router belongs to. + */ + long getAccountId(); + + /** + * @return domain id that the domain router belongs to. + */ + long getDomainId(); + + Role getRole(); + + /** + * @return the range of dhcp addresses served (start and end) + */ + String[] getDhcpRange(); +} diff --git a/core/src/com/cloud/vm/DomainRouterVO.java b/core/src/com/cloud/vm/DomainRouterVO.java new file mode 100755 index 00000000000..13f6c94c939 --- /dev/null +++ b/core/src/com/cloud/vm/DomainRouterVO.java @@ -0,0 +1,333 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +import javax.persistence.Column; +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.Table; + +import com.cloud.utils.net.NetUtils; + +/** + * VirtualMachineRouterVO implements all the fields stored for a domain router. + */ +@Entity +@Table(name="domain_router") +@PrimaryKeyJoinColumn(name="id") +@DiscriminatorValue(value="DomainRouter") +public class DomainRouterVO extends VMInstanceVO implements DomainRouter { + @Column(name="account_id", updatable=false, nullable=false) + private long accountId = -1; + + @Column(name="domain_id", updatable=false, nullable=false) + private long domainId = 0; + + @Column(name="ram_size", updatable=false, nullable=false) + private int ramSize; + + @Column(name="gateway") + private String gateway; + + @Column(name="public_ip_address") + private String publicIpAddress; + + @Column(name="public_mac_address") + private String publicMacAddress; + + @Column(name="public_netmask") + private String publicNetmask; + + @Column(name="vlan_db_id") + private Long vlanDbId; + + @Column(name="vlan_id") + private String vlanId; + + @Column(name="guest_mac_address") + private String guestMacAddress; + + @Column(name="guest_ip_address") + private String guestIpAddress; + + @Column(name="guest_netmask") + private String guestNetmask; + + @Column(name="dns1") + private String dns1; + + @Column(name="dns2") + private String dns2; + + @Column(name="domain", nullable=false) + private String domain; + + @Column(name="vnet") + private String vnet; + + @Column(name="dc_vlan") + private String zoneVlan; + + @Column(name="guest_dc_mac_address") + private String guestZoneMacAddress; + + @Column(name="role") + @Enumerated(EnumType.STRING) + private Role role = Role.DHCP_FIREWALL_LB_PASSWD_USERDATA; + + public DomainRouterVO(long id, + String name, + String instanceName, + String privateMacAddress, + String privateIpAddress, + String privateNetmask, + long templateId, + long guestOSId, + String guestMacAddress, + String guestIpAddress, + String guestNetmask, + String vnet, + long accountId, + long domainId, + String publicMacAddress, + String publicIpAddress, + String publicNetMask, + Long vlanDbId, + String vlanId, + long podId, + long dataCenterId, + int ramSize, + String gateway, + String domain, + Long hostId, + String dns1, + String dns2) { + super(id, name, instanceName, Type.DomainRouter, templateId, guestOSId, privateMacAddress, privateIpAddress, privateNetmask, dataCenterId, podId, true, hostId); + this.privateMacAddress = privateMacAddress; + this.guestMacAddress = guestMacAddress; + this.guestIpAddress = guestIpAddress; + this.publicIpAddress = publicIpAddress; + this.publicMacAddress = publicMacAddress; + this.publicNetmask = publicNetMask; + this.vlanDbId = vlanDbId; + this.vlanId = vlanId; + this.ramSize = ramSize; + this.gateway = gateway; + this.domain = domain; + this.dns1 = dns1; + this.dns2 = dns2; + this.dataCenterId = dataCenterId; + this.vnet = vnet; + this.accountId = accountId; + this.domainId = domainId; + this.guestNetmask = guestNetmask; + } + + public DomainRouterVO(long id, + String name, + String privateMacAddress, + String privateIpAddress, + String privateNetmask, + long templateId, + long guestOSId, + String guestMacAddress, + String guestIpAddress, + String guestNetmask, + long accountId, + long domainId, + String publicMacAddress, + String publicIpAddress, + String publicNetMask, + Long vlanDbId, String vlanId, + long podId, + long dataCenterId, + int ramSize, + String gateway, + String domain, + String dns1, + String dns2) { + this(id, name, name, privateMacAddress, privateIpAddress, privateNetmask, templateId, guestOSId, guestMacAddress, guestIpAddress, guestNetmask, null, accountId, domainId, publicMacAddress, publicIpAddress, publicNetMask, vlanDbId, vlanId, podId, dataCenterId, ramSize, gateway, domain, null, dns1, dns2); + } + + public long getAccountId() { + return accountId; + } + + public long getDomainId() { + return domainId; + } + + public void setGateway(String gateway) { + this.gateway = gateway; + } + + public void setPublicIpAddress(String publicIpAddress) { + this.publicIpAddress = publicIpAddress; + } + + public void setPublicMacAddress(String publicMacAddress) { + this.publicMacAddress = publicMacAddress; + } + + public void setPublicNetmask(String publicNetmask) { + this.publicNetmask = publicNetmask; + } + + public void setGuestMacAddress(String routerMacAddress) { + this.guestMacAddress = routerMacAddress; + } + + @Override + public String getGuestNetmask() { + return guestNetmask; + } + + public void setGuestIpAddress(String routerIpAddress) { + this.guestIpAddress = routerIpAddress; + } + + public void setDns1(String dns1) { + this.dns1 = dns1; + } + + public void setDns2(String dns2) { + this.dns2 = dns2; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public void setVnet(String vnet) { + this.vnet = vnet; + } + + @Override + public String getVnet() { + return vnet; + } + + @Override + public long getDataCenterId() { + return dataCenterId; + } + + @Override + public String getPublicNetmask() { + return publicNetmask; + } + + @Override + public String getPublicMacAddress() { + return publicMacAddress; + } + + @Override + public String getGuestIpAddress() { + return guestIpAddress; + } + + protected DomainRouterVO() { + super(); + } + + @Override + public String getDns1() { + return dns1; + } + + @Override + public String getDns2() { + return dns2; + } + + @Override + public String getDomain() { + return domain; + } + + @Override + public int getRamSize() { + return ramSize; + } + + @Override + public String getGateway() { + return gateway; + } + + @Override + public String getPublicIpAddress() { + return publicIpAddress; + } + + @Override + public String getVlanId() { + return vlanId; + } + + @Override + public String getGuestMacAddress() { + return guestMacAddress; + } + + public void setVlanDbId(Long vlanDbId) { + this.vlanDbId = vlanDbId; + } + + public Long getVlanDbId() { + return vlanDbId; + } + + @Override + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + @Override + public String[] getDhcpRange() { + if (guestIpAddress != null && guestNetmask != null) { + long cidrSize = NetUtils.getCidrSize(guestNetmask); + return NetUtils.getIpRangeFromCidr(guestIpAddress, cidrSize); + } + return new String[2]; + } + + public void setZoneVlan(String zoneVlan) { + this.zoneVlan = zoneVlan; + } + + public String getZoneVlan() { + return zoneVlan; + } + + public void setGuestZoneMacAddress(String guestZoneMacAddress) { + this.guestZoneMacAddress = guestZoneMacAddress; + } + + public String getGuestZoneMacAddress() { + return guestZoneMacAddress; + } +} diff --git a/core/src/com/cloud/vm/SecondaryStorageVm.java b/core/src/com/cloud/vm/SecondaryStorageVm.java new file mode 100644 index 00000000000..139bf23f6db --- /dev/null +++ b/core/src/com/cloud/vm/SecondaryStorageVm.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +import java.util.Date; + +/** + * Secondary Storage VM is a system VM instance that is used + * to interface the management server to secondary storage + */ +public interface SecondaryStorageVm extends VirtualMachine { + + public String getGateway(); + public String getDns1(); + public String getDns2(); + public String getDomain(); + public String getPublicIpAddress(); + public String getPublicNetmask(); + public String getPublicMacAddress(); + public Long getVlanDbId(); + public String getVlanId(); + public String getPrivateNetmask(); + public int getRamSize(); + public Date getLastUpdateTime(); +} + diff --git a/core/src/com/cloud/vm/SecondaryStorageVmVO.java b/core/src/com/cloud/vm/SecondaryStorageVmVO.java new file mode 100644 index 00000000000..716af5b9fb0 --- /dev/null +++ b/core/src/com/cloud/vm/SecondaryStorageVmVO.java @@ -0,0 +1,277 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +/** + * SecondaryStorageVmVO domain object + */ + +@Entity +@Table(name="secondary_storage_vm") +@PrimaryKeyJoinColumn(name="id") +@DiscriminatorValue(value="SecondaryStorageVm") +public class SecondaryStorageVmVO extends VMInstanceVO implements SecondaryStorageVm { + + @Column(name="gateway", nullable=false) + private String gateway; + + @Column(name="dns1") + private String dns1; + + @Column(name="dns2") + private String dns2; + + @Column(name="public_ip_address", nullable=false) + private String publicIpAddress; + + @Column(name="public_mac_address", nullable=false) + private String publicMacAddress; + + @Column(name="public_netmask", nullable=false) + private String publicNetmask; + + @Column(name="guest_mac_address") + private String guestMacAddress; + + @Column(name="guest_ip_address") + private String guestIpAddress; + + @Column(name="guest_netmask") + private String guestNetmask; + + @Column(name="vlan_db_id") + private Long vlanDbId; + + @Column(name="vlan_id") + private String vlanId; + + @Column(name="domain", nullable=false) + private String domain; + + @Column(name="guid", nullable=false) + private String guid; + + @Column(name="nfs_share", nullable=false) + private String nfsShare; + + + @Column(name="ram_size", updatable=false, nullable=false) + private int ramSize; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name="last_update", updatable=true, nullable=true) + private Date lastUpdateTime; + + + public SecondaryStorageVmVO( + long id, + String name, + String guestMacAddress, + String guestIpAddress, + String guestNetMask, + String privateMacAddress, + String privateIpAddress, + String privateNetmask, + long templateId, + long guestOSId, + String publicMacAddress, + String publicIpAddress, + String publicNetmask, + Long vlanDbId, + String vlanId, + long podId, + long dataCenterId, + String gateway, + Long hostId, + String dns1, + String dns2, + String domain, + int ramSize, + String guid, + String nfsShare) { + super(id, name, name, Type.SecondaryStorageVm, templateId, guestOSId, + privateMacAddress, privateIpAddress, privateNetmask, dataCenterId, podId, true, hostId); + this.gateway = gateway; + this.publicIpAddress = publicIpAddress; + this.publicNetmask = publicNetmask; + this.publicMacAddress = publicMacAddress; + this.guestIpAddress = guestIpAddress; + this.guestMacAddress = guestMacAddress; + this.guestNetmask = guestNetMask; + this.vlanDbId = vlanDbId; + this.vlanId = vlanId; + this.dns1 = dns1; + this.dns2 = dns2; + this.domain = domain; + this.ramSize = ramSize; + this.setGuid(guid); + this.nfsShare = nfsShare; + } + + protected SecondaryStorageVmVO() { + super(); + } + + public void setGateway(String gateway) { + this.gateway = gateway; + } + + public void setDns1(String dns1) { + this.dns1 = dns1; + } + + public void setDns2(String dns2) { + this.dns2 = dns2; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public void setPublicIpAddress(String publicIpAddress) { + this.publicIpAddress = publicIpAddress; + } + + public void setPublicNetmask(String publicNetmask) { + this.publicNetmask = publicNetmask; + } + + public void setPublicMacAddress(String publicMacAddress) { + this.publicMacAddress = publicMacAddress; + } + + public void setGuestIpAddress(String guestIpAddress) { + this.guestIpAddress = guestIpAddress; + } + + public void setGuestNetmask(String guestNetmask) { + this.guestNetmask = guestNetmask; + } + + public void setGuestMacAddress(String guestMacAddress) { + this.guestMacAddress = guestMacAddress; + } + + public void setRamSize(int ramSize) { + this.ramSize = ramSize; + } + + public void setLastUpdateTime(Date time) { + this.lastUpdateTime = time; + } + + @Override + public String getGateway() { + return this.gateway; + } + + @Override + public String getDns1() { + return this.dns1; + } + + @Override + public String getDns2() { + return this.dns2; + } + + @Override + public String getPublicIpAddress() { + return this.publicIpAddress; + } + + @Override + public String getPublicNetmask() { + return this.publicNetmask; + } + + @Override + public String getPublicMacAddress() { + return this.publicMacAddress; + } + + + public String getGuestIpAddress() { + return this.guestIpAddress; + } + + + public String getGuestNetmask() { + return this.guestNetmask; + } + + + public String getGuestMacAddress() { + return this.guestMacAddress; + } + + @Override + public Long getVlanDbId() { + return vlanDbId; + } + + @Override + public String getVlanId() { + return vlanId; + } + + @Override + public String getDomain() { + return this.domain; + } + + @Override + public int getRamSize() { + return this.ramSize; + } + + + @Override + public Date getLastUpdateTime() { + return this.lastUpdateTime; + } + + public void setGuid(String guid) { + this.guid = guid; + } + + public String getGuid() { + return guid; + } + + public void setNfsShare(String nfsShare) { + this.nfsShare = nfsShare; + } + + public String getNfsShare() { + return nfsShare; + } + + + +} diff --git a/core/src/com/cloud/vm/UserVmVO.java b/core/src/com/cloud/vm/UserVmVO.java new file mode 100755 index 00000000000..0943ff72f2b --- /dev/null +++ b/core/src/com/cloud/vm/UserVmVO.java @@ -0,0 +1,264 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +import javax.persistence.Column; +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.Table; + +import com.cloud.uservm.UserVm; + +@Entity +@Table(name="user_vm") +@DiscriminatorValue(value="User") +@PrimaryKeyJoinColumn(name="id") +public class UserVmVO extends VMInstanceVO implements UserVm { + + @Column(name="account_id", updatable=false, nullable=false) + private long accountId = -1; + + @Column(name="domain_id", updatable=false, nullable=false) + private long domainId = -1; + + @Column(name="domain_router_id", updatable=true, nullable=true) + Long domainRouterId; + + @Column(name="service_offering_id", updatable=true, nullable=false) + long serviceOfferingId; + + @Column(name="vnet", length=10, updatable=true, nullable=true) + String vnet; + + @Column(name="guest_ip_address") + String guestIpAddress; + + @Column(name="guest_mac_address") + String guestMacAddress; + + @Column(name="guest_netmask") + String guestNetmask; + + @Column(name="iso_id", nullable=true, length=17) + private Long isoId = null; + + @Column(name="external_ip_address") + String externalIpAddress; + + @Column(name="group", updatable=true, nullable=true) + private String group; + + @Column(name="external_mac_address") + String externalMacAddress; + + @Column(name="external_vlan_db_id") + private Long externalVlanDbId; + + @Column(name="user_data", updatable=true, nullable=true, length=2048) + private String userData; + + @Column(name="display_name", updatable=true, nullable=true) + private String displayName; + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public long getDomainId() { + return domainId; + } + + public String getGuestIpAddress() { + return guestIpAddress; + } + + public void setGuestIpAddress(String guestIpAddress) { + this.guestIpAddress = guestIpAddress; + setPrivateIpAddress(guestIpAddress); + } + + public String getGuestMacAddress() { + return guestMacAddress; + } + + public void setGuestMacAddress(String guestMacAddress) { + this.guestMacAddress = guestMacAddress; + setPrivateMacAddress(guestMacAddress); + + } + + public String getGuestNetmask() { + return guestNetmask; + } + + public void setGuestNetmask(String guestNetmask) { + this.guestNetmask = guestNetmask; + setPrivateNetmask(guestNetmask); + } + + @Override + public Long getIsoId() { + return isoId; + } + + @Override + public Long getDomainRouterId() { + return domainRouterId; + } + + public void setDomainRouterId(long domainRouterId) { + this.domainRouterId = domainRouterId; + } + + public void setVnet(String vnet) { + this.vnet = vnet; + } + + @Override + public long getServiceOfferingId() { + return serviceOfferingId; + } + + public void setServiceOfferingId(long serviceOfferingId) { + this.serviceOfferingId = serviceOfferingId; + } + + @Override + public String getVnet() { + return vnet; + } + + @Override + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public UserVmVO(long id, + String instanceName, + String displayName, + long templateId, + long guestOsId, + boolean haEnabled, + long domainId, + long accountId, + long serviceOfferingId, + String group, + String userData) { + super(id, displayName, instanceName, Type.User, templateId, guestOsId, haEnabled); + this.group = group; + this.userData = userData; + this.displayName = displayName; + + } + + public UserVmVO(long id, + String name, + long templateId, + long guestOSId, + long accountId, + long domainId, + long serviceOfferingId, + String guestMacAddress, + String guestIpAddress, + String guestNetMask, + String externalIpAddress, + String externalMacAddress, + Long vlanDbId, + Long routerId, + long podId, + long dcId, + boolean haEnabled, + String displayName, + String group, + String userData) { + super(id, name, name, Type.User, templateId, guestOSId, guestMacAddress, guestIpAddress, guestNetMask, dcId, podId, haEnabled, null); + this.serviceOfferingId = serviceOfferingId; + this.domainRouterId = routerId; + this.accountId = accountId; + this.domainId = domainId; + this.guestIpAddress = guestIpAddress; + this.guestNetmask = guestNetMask; + this.guestMacAddress = guestMacAddress; + this.externalIpAddress = externalIpAddress; + this.externalMacAddress = externalMacAddress; + this.setUserData(userData); + this.setExternalVlanDbId(vlanDbId); + this.group = group; + this.isoId = null; + this.displayName = displayName; + this.group = group; + } + + protected UserVmVO() { + super(); + } + + public String getExternalIpAddress() { + return externalIpAddress; + } + + public void setIsoId(Long id) { + this.isoId = id; + } + + public void setExternalIpAddress(String externalIpAddress) { + this.externalIpAddress = externalIpAddress; + } + + public String getExternalMacAddress() { + return externalMacAddress; + } + + public void setExternalMacAddress(String externalMacAddress) { + this.externalMacAddress = externalMacAddress; + } + + public void setExternalVlanDbId(Long vlanDbId) { + this.externalVlanDbId = vlanDbId; + } + + public Long getExternalVlanDbId() { + return externalVlanDbId; + } + + @Override + public void setUserData(String userData) { + this.userData = userData; + } + + @Override + public String getUserData() { + return userData; + } + + @Override + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } +} diff --git a/core/src/com/cloud/vm/VMInstanceVO.java b/core/src/com/cloud/vm/VMInstanceVO.java new file mode 100644 index 00000000000..84c91116737 --- /dev/null +++ b/core/src/com/cloud/vm/VMInstanceVO.java @@ -0,0 +1,372 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.vm; + +import java.util.Date; +import java.util.Random; + +import javax.persistence.Column; +import javax.persistence.DiscriminatorColumn; +import javax.persistence.DiscriminatorType; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.Table; +import javax.persistence.TableGenerator; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name="vm_instance") +@Inheritance(strategy=InheritanceType.JOINED) +@DiscriminatorColumn(name="type", discriminatorType=DiscriminatorType.STRING, length=32) +public class VMInstanceVO implements VirtualMachine { + @Id + @TableGenerator(name="vm_instance_sq", table="sequence", pkColumnName="name", valueColumnName="value", pkColumnValue="vm_instance_seq", allocationSize=1) + @Column(name="id", updatable=false, nullable = false) + private long id; + + @Column(name="name", updatable=false, nullable=false, length=255) + private String name = null; + + @Column(name="vnc_password", updatable=true, nullable=false, length=255) + String vncPassword; + + @Column(name="proxy_id", updatable=true, nullable=true) + Long proxyId; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name="proxy_assign_time", updatable=true, nullable=true) + Date proxyAssignTime; + + /** + * Note that state is intentionally missing the setter. Any updates to + * the state machine needs to go through the DAO object because someone + * else could be updating it as well. + */ + @Enumerated(value=EnumType.STRING) + @Column(name="state", updatable=true, nullable=false, length=32) + private State state = null; + + @Column(name="private_ip_address", updatable=true) + private String privateIpAddress; + + @Column(name="instance_name", updatable=true, nullable=false) + private String instanceName; + + @Column(name="vm_template_id", updatable=false, nullable=true, length=17) + private Long templateId = new Long(-1); + + @Column(name="guest_os_id", nullable=false, length=17) + private long guestOSId; + + @Column(name="host_id", updatable=true, nullable=true) + private Long hostId; + + @Column(name="last_host_id", updatable=true, nullable=true) + private Long lastHostId; + + @Column(name="pod_id", updatable=true, nullable=false) + private long podId; + + @Column(name="private_mac_address", updatable=true, nullable=true) + String privateMacAddress; + + @Column(name="private_netmask") + private String privateNetmask; + + @Column(name="data_center_id", updatable=true, nullable=false) + long dataCenterId; + + @Column(name="type", updatable=false, nullable=false, length=32) + @Enumerated(value=EnumType.STRING) + Type type; + + @Column(name="ha_enabled", updatable=true, nullable=true) + boolean haEnabled; + + @Column(name="mirrored_vols", updatable=true, nullable=true) + boolean mirroredVols; + + @Column(name="update_count", updatable = true, nullable=false) + long updated; // This field should be updated everytime the state is updated. There's no set method in the vo object because it is done with in the dao code. + + @Column(name=GenericDao.CREATED_COLUMN) + Date created; + + @Column(name=GenericDao.REMOVED_COLUMN) + Date removed; + + @Column(name="update_time", updatable=true) + @Temporal(value=TemporalType.TIMESTAMP) + Date updateTime; + + public VMInstanceVO(long id, + String name, + String instanceName, + Type type, + Long vmTemplateId, + long guestOSId, + boolean haEnabled) { + this.id = id; + this.name = name; + if (vmTemplateId != null) { + this.templateId = vmTemplateId; + } + this.instanceName = instanceName; + this.type = type; + this.guestOSId = guestOSId; + this.haEnabled = haEnabled; + } + + + public VMInstanceVO(long id, + String name, + String instanceName, + Type type, + long vmTemplateId, + long guestOSId, + String privateMacAddress, + String privateIpAddress, + String privateNetmask, + long dataCenterId, + long podId, + boolean haEnabled, + Long hostId) { + super(); + this.id = id; + this.name = name; + if (vmTemplateId > -1) + this.templateId = vmTemplateId; + else + this.templateId = null; + this.guestOSId = guestOSId; + this.privateIpAddress = privateIpAddress; + this.privateMacAddress = privateMacAddress; + this.privateNetmask = privateNetmask; + this.hostId = hostId; + this.dataCenterId = dataCenterId; + this.podId = podId; + this.type = type; + this.haEnabled = haEnabled; + this.instanceName = instanceName; + this.updated = 0; + this.updateTime = new Date(); + this.vncPassword = Long.toHexString(new Random().nextLong()); + this.state = State.Creating; + } + + protected VMInstanceVO() { + } + + public Date getRemoved() { + return removed; + } + + @Override + public Type getType() { + return type; + } + + public long getUpdated() { + return updated; + } + + public long getId() { + return id; + } + + public Date getCreated() { + return created; + } + + public Date getUpdateTime() { + return updateTime; + } + + public long getDataCenterId() { + return dataCenterId; + } + + public void setPrivateNetmask(String privateNetmask) { + this.privateNetmask = privateNetmask; + } + + public String getPrivateNetmask() { + return privateNetmask; + } + + + public void setId(long id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getInstanceName() { + return instanceName; + } + + public void setInstanceName(String instanceName) { + this.instanceName = instanceName; + } + + @Override + public State getState() { + return state; + } + + // don't use this directly, use VM state machine instead, this method is added for migration tool only + public void setState(State state) { + this.state = state; + } + + @Override + public String getPrivateIpAddress() { + return privateIpAddress; + } + + public void setPrivateIpAddress(String address) { + privateIpAddress = address; + } + + public void setVncPassword(String vncPassword) { + this.vncPassword = vncPassword; + } + + @Override + public String getVncPassword() { + return vncPassword; + } + + public Long getProxyId() { + return proxyId; + } + + public void setProxyId(Long proxyId) { + this.proxyId = proxyId; + } + + public Date getProxyAssignTime() { + return this.proxyAssignTime; + } + + public void setProxyAssignTime(Date time) { + this.proxyAssignTime = time; + } + + @Override + public long getTemplateId() { + if (templateId == null) + return -1; + else + return templateId; + } + + public void setTemplateId(Long templateId) { + this.templateId = templateId; + } + + public long getGuestOSId() { + return guestOSId; + } + + public void setGuestOSId(long guestOSId) { + this.guestOSId = guestOSId; + } + + public void incrUpdated() { + updated++; + } + + @Override + public Long getHostId() { + return hostId; + } + + @Override + public Long getLastHostId() { + return lastHostId; + } + + public void setLastHostId(Long lastHostId) { + this.lastHostId = lastHostId; + } + + public void setHostId(Long hostId) { + this.hostId = hostId; + } + + @Override + public boolean isHaEnabled() { + return haEnabled; + } + + @Override + public String getPrivateMacAddress() { + return privateMacAddress; + } + + @Override + public long getPodId() { + return podId; + } + + public void setPodId(long podId) { + this.podId = podId; + } + + public void setPrivateMacAddress(String privateMacAddress) { + this.privateMacAddress = privateMacAddress; + } + + public void setDataCenterId(long dataCenterId) { + this.dataCenterId = dataCenterId; + } + + public boolean isRemoved() { + return removed != null; + } + + public boolean isMirroredVols() { + return mirroredVols; + } + + public void setHaEnabled(boolean value) { + haEnabled = value; + } + + public void setMirroredVols(boolean mirroredVols) { + this.mirroredVols = mirroredVols; + } + + @Override + public String toString() { + return new StringBuilder("[").append(type.toString()).append("|").append(instanceName).append("]").toString(); + } +} diff --git a/core/src/com/cloud/vm/VirtualDisk.java b/core/src/com/cloud/vm/VirtualDisk.java new file mode 100644 index 00000000000..deaf48d5c0e --- /dev/null +++ b/core/src/com/cloud/vm/VirtualDisk.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +import com.cloud.storage.Storage; + +/** + * VirtualDisk describes the disks that are plugged into + * the virtual machine. + * + */ +public class VirtualDisk { + public Storage.ImageFormat format; + public String url; + public boolean bootable; + public long size; +} diff --git a/core/src/com/cloud/vm/VirtualEnvironment.java b/core/src/com/cloud/vm/VirtualEnvironment.java new file mode 100644 index 00000000000..5ef6b9ca375 --- /dev/null +++ b/core/src/com/cloud/vm/VirtualEnvironment.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +import java.util.List; + +/** + * + * VirtualEnvironment describes the environment that the + * virtual machine exists in. This object is designed to + * be an information carrier within one thread only. + * + */ +public class VirtualEnvironment { + /** + * The actual machine + */ + public VirtualMachine machine; + + /** + * Disks to assign to the machine in order. + */ + public List disks; + + /** + * Networks to assign to the machine. + */ + public List networks; + + /** + * Boot options to assign to the machine. + */ + public String bootOptions; +} diff --git a/core/src/com/cloud/vm/VirtualMachineManager.java b/core/src/com/cloud/vm/VirtualMachineManager.java new file mode 100644 index 00000000000..e22f0ccd1b6 --- /dev/null +++ b/core/src/com/cloud/vm/VirtualMachineManager.java @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +import com.cloud.agent.api.Command; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.exception.StorageUnavailableException; +import com.cloud.host.HostVO; +import com.cloud.utils.exception.ExecutionException; + +/** + * HighAvailabilityHandler specifies the methods that are used to control + * VMs during the sync process and the HA process. While different types of + * VMs have a lot in common, they allocate resources differently and it + * doesn't make sense to + * + */ +public interface VirtualMachineManager { + + /** + * Returns the id parsed from the name. If it cannot parse the name, + * then return null. This method is used to determine if this is + * the right handler for this vm. + * + * @param vmName vm name coming form the agent. + * @return id if the handler works for this vm and can parse id. null if not. + */ + Long convertToId(String vmName); + + /** + * Retrieves the vm based on the id given. + * + * @param id id of the vm. + * @return VMInstanceVO + */ + T get(long id); + + /** + * Complete the start command. HA calls this when it determines that + * a vm was starting but the state was not complete. + * + * @param vm vm to execute this on. + */ + void completeStartCommand(T vm); + + /** + * Complete the stop command. HA calls this when it determines that + * a vm was being stopped but it didn't complete. + * + * @param vm vm to stop. + */ + void completeStopCommand(T vm); + + /** + * start the vm + * + * @param vm to start. + * @return true if started. false if not. + * @throws InsufficientCapacityException if there's not enough capacity to start the vm. + * @throws StorageUnavailableException if the storage is unavailable. + * @throws ConcurrentOperationException there's multiple threads working on this vm. + * @throws ExecutionException + */ + T start(long vmId, long startEventId) throws InsufficientCapacityException, StorageUnavailableException, ConcurrentOperationException, ExecutionException; + + /** + * stop the vm + * + * @param vm vm to Stop. + * @return true if stopped and false if not. + * @throws AgentUnavailableException if the agent is unavailable. + */ + boolean stop(T vm, long startEventId) throws AgentUnavailableException; + + /** + * Produce a cleanup command to be sent to the agent to cleanup anything + * out of the ordinary. + * @param vm vm to cleanup. It's possible this is null. + * @param vmName name of the vm from the agent. + * @return Command to clean it up. If not cleanup is needed, then return null. + */ + Command cleanup(T vm, String vmName); + + /** + * Prepare for migration. + * + * @param vm vm to migrate. + * @return HostVO if a host is found. + */ + HostVO prepareForMigration(T vm) throws InsufficientCapacityException, StorageUnavailableException; + + /** + * Migrate the vm. + */ + boolean migrate(T vm, HostVO host) throws AgentUnavailableException, OperationTimedoutException; + + boolean completeMigration(T vm, HostVO host) throws AgentUnavailableException, OperationTimedoutException; + + boolean destroy(T vm) throws AgentUnavailableException; +} diff --git a/core/src/com/cloud/vm/VirtualMachineName.java b/core/src/com/cloud/vm/VirtualMachineName.java new file mode 100755 index 00000000000..a5d85170417 --- /dev/null +++ b/core/src/com/cloud/vm/VirtualMachineName.java @@ -0,0 +1,195 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +import java.util.Formatter; + +import com.cloud.dc.Vlan; + +/** + * This class contains the different ways to construct and deconstruct a + * VM Name. + */ +public class VirtualMachineName { + public static final String SEPARATOR = "-"; + + public static String getVnetName(long vnetId) { + StringBuilder vnet = new StringBuilder(); + Formatter formatter = new Formatter(vnet); + formatter.format("%04x", vnetId); + return vnet.toString(); + } + + public static boolean isValidVmName(String vmName) { + return isValidVmName(vmName, null); + } + + public static boolean isValidVmName(String vmName, String instance) { + String[] tokens = vmName.split(SEPARATOR); + /*Some vms doesn't have vlan/vnet id*/ + if (tokens.length != 5 && tokens.length != 4) { + return false; + } + + if (!tokens[0].equals("i")) { + return false; + } + + try { + Long.parseLong(tokens[1]); + Long.parseLong(tokens[2]); + if (tokens.length == 5 && !Vlan.UNTAGGED.equalsIgnoreCase(tokens[4])) { + Long.parseLong(tokens[4], 16); + } + } catch (NumberFormatException e) { + return false; + } + + return instance == null || instance.equals(tokens[3]); + } + + public static String getVmName(long vmId, long userId, String instance) { + StringBuilder vmName = new StringBuilder("i"); + vmName.append(SEPARATOR).append(userId).append(SEPARATOR).append(vmId); + vmName.append(SEPARATOR).append(instance); + return vmName.toString(); + } + + public static long getVmId(String vmName) { + int begin = vmName.indexOf(SEPARATOR); + begin = vmName.indexOf(SEPARATOR, begin + SEPARATOR.length()); + int end = vmName.indexOf(SEPARATOR, begin + SEPARATOR.length()); + return Long.parseLong(vmName.substring(begin + 1, end)); + } + + public static long getRouterId(String routerName) { + int begin = routerName.indexOf(SEPARATOR); + int end = routerName.indexOf(SEPARATOR, begin + SEPARATOR.length()); + return Long.parseLong(routerName.substring(begin + 1, end)); + } + + public static long getConsoleProxyId(String vmName) { + int begin = vmName.indexOf(SEPARATOR); + int end = vmName.indexOf(SEPARATOR, begin + SEPARATOR.length()); + return Long.parseLong(vmName.substring(begin + 1, end)); + } + + public static long getSystemVmId(String vmName) { + int begin = vmName.indexOf(SEPARATOR); + int end = vmName.indexOf(SEPARATOR, begin + SEPARATOR.length()); + return Long.parseLong(vmName.substring(begin + 1, end)); + } + + public static String getVnet(String vmName) { + return vmName.substring(vmName.lastIndexOf(SEPARATOR) + SEPARATOR.length()); + } + + public static String getRouterName(long routerId, String instance) { + StringBuilder builder = new StringBuilder("r"); + builder.append(SEPARATOR).append(routerId).append(SEPARATOR).append(instance); + return builder.toString(); + } + + public static String getConsoleProxyName(long vmId, String instance) { + StringBuilder builder = new StringBuilder("v"); + builder.append(SEPARATOR).append(vmId).append(SEPARATOR).append(instance); + return builder.toString(); + } + + public static String getSystemVmName(long vmId, String instance, String prefix) { + StringBuilder builder = new StringBuilder(prefix); + builder.append(SEPARATOR).append(vmId).append(SEPARATOR).append(instance); + return builder.toString(); + } + + public static String attachVnet(String name, String vnet) { + return name + SEPARATOR + vnet; + } + + public static boolean isValidRouterName(String name) { + return isValidRouterName(name, null); + } + + public static boolean isValidRouterName(String name, String instance) { + String[] tokens = name.split(SEPARATOR); + if (tokens.length != 4) { + return false; + } + + if (!tokens[0].equals("r")) { + return false; + } + + try { + Long.parseLong(tokens[1]); + if (!Vlan.UNTAGGED.equalsIgnoreCase(tokens[3])) { + Long.parseLong(tokens[3], 16); + } + } catch (NumberFormatException ex) { + return false; + } + + return instance == null || tokens[2].equals(instance); + } + + public static boolean isValidConsoleProxyName(String name) { + return isValidConsoleProxyName(name, null); + } + + public static boolean isValidConsoleProxyName(String name, String instance) { + String[] tokens = name.split(SEPARATOR); + if (tokens.length != 3) { + return false; + } + + if (!tokens[0].equals("v")) { + return false; + } + + try { + Long.parseLong(tokens[1]); + } catch (NumberFormatException ex) { + return false; + } + + return instance == null || tokens[2].equals(instance); + } + + public static boolean isValidSecStorageVmName(String name, String instance) { + return isValidSystemVmName(name, instance, "s"); + } + + public static boolean isValidSystemVmName(String name, String instance, String prefix) { + String[] tokens = name.split(SEPARATOR); + if (tokens.length != 3) { + return false; + } + + if (!tokens[0].equals(prefix)) { + return false; + } + + try { + Long.parseLong(tokens[1]); + } catch (NumberFormatException ex) { + return false; + } + + return instance == null || tokens[2].equals(instance); + } +} diff --git a/core/src/com/cloud/vm/VirtualNetwork.java b/core/src/com/cloud/vm/VirtualNetwork.java new file mode 100644 index 00000000000..fd882a0b7e9 --- /dev/null +++ b/core/src/com/cloud/vm/VirtualNetwork.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm; + +/** + * VirtualNetwork describes from a management level the + * things needed to provide the network to the virtual + * machine. + */ +public class VirtualNetwork { + public enum Mode { + None, + Local, + Static, + Dhcp; + } + + public enum Isolation { + VNET, + VLAN, + OSWITCH, + } + + /** + * The gateway for this network. + */ + public String gateway; + + /** + * Netmask + */ + public String netmask; + + /** + * ip address. null if mode is DHCP. + */ + public String ip; + + /** + * Mac Address. + */ + public String mac; + + /** + * rate limit on this network. -1 if no limit. + */ + public long rate; + + /** + * tag for virtualization. + */ + public String tag; + + /** + * mode to acquire ip address. + */ + public Mode mode; + + /** + * Isolation method for networking. + */ + public Isolation method; + + public boolean firewalled; + + public int[] openPorts; + + public int[] closedPorts; +} diff --git a/core/src/com/cloud/vm/VmStats.java b/core/src/com/cloud/vm/VmStats.java new file mode 100644 index 00000000000..1056bb18465 --- /dev/null +++ b/core/src/com/cloud/vm/VmStats.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.vm; + +public interface VmStats { + //vm related stats + public double getCPUUtilization(); + public double getNetworkReadKBs(); + public double getNetworkWriteKBs(); + +} diff --git a/core/src/com/cloud/vm/dao/ConsoleProxyDao.java b/core/src/com/cloud/vm/dao/ConsoleProxyDao.java new file mode 100644 index 00000000000..f6ab6c94cff --- /dev/null +++ b/core/src/com/cloud/vm/dao/ConsoleProxyDao.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.vm.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.info.ConsoleProxyLoadInfo; +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.ConsoleProxyVO; +import com.cloud.vm.State; +import com.cloud.vm.VirtualMachine; + +public interface ConsoleProxyDao extends GenericDao { + + public void update(long id, int activeSession, Date updateTime, byte[] sessionDetails); + + public List getProxyListInStates(long dataCenterId, State... states); + public List getProxyListInStates(State... states); + + public List listByHostId(long hostId); + public List listUpByHostId(long hostId); + + public List getDatacenterProxyLoadMatrix(); + public List getDatacenterVMLoadMatrix(); + public List getDatacenterSessionLoadMatrix(); + public List> getDatacenterStoragePoolHostInfo(long dcId, boolean countAllPoolTypes); + public List> getProxyLoadMatrix(); + public int getProxyStaticLoad(long proxyVmId); + public int getProxyActiveLoad(long proxyVmId); + public List getRunningProxyListByMsid(long msid); + + public boolean updateIf(ConsoleProxyVO vm, VirtualMachine.Event event, Long hostId); +} diff --git a/core/src/com/cloud/vm/dao/ConsoleProxyDaoImpl.java b/core/src/com/cloud/vm/dao/ConsoleProxyDaoImpl.java new file mode 100644 index 00000000000..2d33f98aeb4 --- /dev/null +++ b/core/src/com/cloud/vm/dao/ConsoleProxyDaoImpl.java @@ -0,0 +1,390 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.vm.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.info.ConsoleProxyLoadInfo; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.UpdateBuilder; +import com.cloud.vm.ConsoleProxyVO; +import com.cloud.vm.State; +import com.cloud.vm.VirtualMachine; + +@Local(value={ConsoleProxyDao.class}) +public class ConsoleProxyDaoImpl extends GenericDaoBase implements ConsoleProxyDao { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyDaoImpl.class); + + // + // query SQL for returnning console proxy assignment info as following + // proxy vm id, count of assignment + // + private static final String PROXY_ASSIGNMENT_MATRIX = + "SELECT c.id, count(runningVm.id) AS count " + + " FROM console_proxy AS c LEFT JOIN" + + " (SELECT v.id AS id, v.proxy_id AS proxy_id FROM vm_instance AS v WHERE " + + " (v.state='Running' OR v.state='Creating' OR v.state='Starting' OR v.state='Migrating')) " + + " AS runningVm ON c.id = runningVm.proxy_id" + + " GROUP BY c.id"; + + // + // query SQL for returnning running VM count at data center basis + // + private static final String DATACENTER_VM_MATRIX = + "SELECT d.id, d.name, count(v.id) AS count" + + " FROM data_center AS d LEFT JOIN vm_instance AS v ON v.data_center_id=d.id " + + " WHERE (v.state='Creating' OR v.state='Starting' OR v.state='Running' OR v.state='Migrating')" + + " GROUP BY d.id, d.name"; + + private static final String DATACENTER_ACTIVE_SESSION_MATRIX = + "SELECT d.id, d.name, sum(c.active_session) AS count" + + " FROM data_center AS d LEFT JOIN vm_instance AS v ON v.data_center_id=d.id " + + " LEFT JOIN console_proxy AS c ON v.id=c.id " + + " WHERE v.type='ConsoleProxy' AND (v.state='Creating' OR v.state='Starting' OR v.state='Running' OR v.state='Migrating')" + + " GROUP BY d.id, d.name"; + + // + // query SQL for returnning running console proxy count at data center basis + // + private static final String DATACENTER_PROXY_MATRIX = + "SELECT d.id, d.name, count(dcid) as count" + + " FROM data_center as d" + + " LEFT JOIN (" + + " SELECT v.data_center_id as dcid, c.active_session as active_session from vm_instance as v" + + " INNER JOIN console_proxy as c ON v.id=c.id AND v.type='ConsoleProxy' AND (v.state='Creating' OR v.state='Starting' OR v.state='Running' OR v.state='Migrating')" + + " ) as t ON d.id = t.dcid" + + " GROUP BY d.id, d.name"; + + private static final String GET_PROXY_LOAD = + "SELECT count(*) AS count" + + " FROM vm_instance AS v " + + " WHERE v.proxy_id=? AND (v.state='Running' OR v.state='Starting' OR v.state='Creating' OR v.state='Migrating')"; + + private static final String GET_PROXY_ACTIVE_LOAD = + "SELECT active_session AS count" + + " FROM console_proxy" + + " WHERE proxy_id=?"; + + private static final String STORAGE_POOL_HOST_INFO = + "SELECT p.data_center_id, count(ph.host_id) " + + " FROM storage_pool p, storage_pool_host_ref ph " + + " WHERE p.id = ph.pool_id AND p.data_center_id = ? " + + " GROUP by p.data_center_id"; + + private static final String SHARED_STORAGE_POOL_HOST_INFO = + "SELECT p.data_center_id, count(ph.host_id) " + + " FROM storage_pool p, storage_pool_host_ref ph " + + " WHERE p.pool_type <> 'LVM' AND p.id = ph.pool_id AND p.data_center_id = ? " + + " GROUP by p.data_center_id"; + + protected SearchBuilder DataCenterStatusSearch; + protected SearchBuilder StateSearch; + protected SearchBuilder HostSearch; + protected SearchBuilder HostUpSearch; + protected SearchBuilder StateChangeSearch; + + protected final Attribute _updateTimeAttr; + + public ConsoleProxyDaoImpl() { + DataCenterStatusSearch = createSearchBuilder(); + DataCenterStatusSearch.and("dc", DataCenterStatusSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DataCenterStatusSearch.and("states", DataCenterStatusSearch.entity().getState(), SearchCriteria.Op.IN); + DataCenterStatusSearch.done(); + + StateSearch = createSearchBuilder(); + StateSearch.and("states", StateSearch.entity().getState(), SearchCriteria.Op.IN); + StateSearch.done(); + + HostSearch = createSearchBuilder(); + HostSearch.and("host", HostSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostSearch.done(); + + HostUpSearch = createSearchBuilder(); + HostUpSearch.and("host", HostUpSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostUpSearch.and("states", HostUpSearch.entity().getState(), SearchCriteria.Op.NIN); + HostUpSearch.done(); + + StateChangeSearch = createSearchBuilder(); + StateChangeSearch.and("id", StateChangeSearch.entity().getId(), SearchCriteria.Op.EQ); + StateChangeSearch.and("states", StateChangeSearch.entity().getState(), SearchCriteria.Op.EQ); + StateChangeSearch.and("host", StateChangeSearch.entity().getHostId(), SearchCriteria.Op.EQ); + StateChangeSearch.and("update", StateChangeSearch.entity().getUpdated(), SearchCriteria.Op.EQ); + StateChangeSearch.done(); + + _updateTimeAttr = _allAttributes.get("updateTime"); + assert _updateTimeAttr != null : "Couldn't get this updateTime attribute"; + } + + @Override + public boolean updateIf(ConsoleProxyVO vm, VirtualMachine.Event event, Long hostId) { + State oldState = vm.getState(); + State newState = oldState.getNextState(event); + + Long oldHostId = vm.getHostId(); + long oldDate = vm.getUpdated(); + + if (newState == null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("There's no way to transition from old state: " + oldState.toString() + " event: " + event.toString()); + } + return false; + } + + SearchCriteria sc = StateChangeSearch.create(); + sc.setParameters("id", vm.getId()); + sc.setParameters("states", oldState); + sc.setParameters("host", vm.getHostId()); + sc.setParameters("update", vm.getUpdated()); + + vm.incrUpdated(); + UpdateBuilder ub = getUpdateBuilder(vm); + + if(newState == State.Running) { + // save current running host id + ub.set(vm, "lastHostId", vm.getHostId()); + } + + ub.set(vm, _updateTimeAttr, new Date()); + ub.set(vm, "state", newState); + ub.set(vm, "hostId", hostId); + + if (newState == State.Stopped) { + vm.setActiveSession(0); + ub.set(vm, "hostId", null); + } + + int result = update(vm, sc); + + if (result == 0 && s_logger.isDebugEnabled()) { + ConsoleProxyVO vo = findById(vm.getId()); + StringBuilder str = new StringBuilder("Unable to update ").append(vo.toString()); + str.append(": DB Data={Host=").append(vo.getHostId()).append("; State=").append(vo.getState().toString()).append("; updated=").append(vo.getUpdated()); + str.append("} New Data: {Host=").append(vm.getHostId()).append("; State=").append(vm.getState().toString()).append("; updated=").append(vm.getUpdated()); + str.append("} Stale Data: {Host=").append(oldHostId).append("; State=").append(oldState.toString()).append("; updated=").append(oldDate).append("}"); + s_logger.debug(str.toString()); + } + + return result > 0; + } + + + @Override + public void update(long id, int activeSession, Date updateTime, byte[] sessionDetails) { + ConsoleProxyVO ub = createForUpdate(); + ub.setActiveSession(activeSession); + ub.setLastUpdateTime(updateTime); + ub.setSessionDetails(sessionDetails); + + update(id, ub); + } + + @Override + public boolean remove(Long id) { + Transaction txn = Transaction.currentTxn(); + txn.start(); + ConsoleProxyVO proxy = createForUpdate(); + proxy.setPublicIpAddress(null); + proxy.setPrivateIpAddress(null); + + UpdateBuilder ub = getUpdateBuilder(proxy); + ub.set(proxy, "state", State.Destroyed); + ub.set(proxy, "privateIpAddress", null); + update(id, ub); + + boolean result = super.remove(id); + txn.commit(); + return result; + } + + @Override + public List getProxyListInStates(long dataCenterId, State... states) { + SearchCriteria sc = DataCenterStatusSearch.create(); + sc.setParameters("states", (Object[])states); + sc.setParameters("dc", dataCenterId); + return listActiveBy(sc); + } + + @Override + public List getProxyListInStates(State... states) { + SearchCriteria sc = StateSearch.create(); + sc.setParameters("states", (Object[])states); + return listActiveBy(sc); + } + + @Override + public List listByHostId(long hostId) { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("host", hostId); + return listActiveBy(sc); + } + + @Override + public List listUpByHostId(long hostId) { + SearchCriteria sc = HostUpSearch.create(); + sc.setParameters("host", hostId); + sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging}); + return listActiveBy(sc); + } + + @Override + public List getDatacenterProxyLoadMatrix() { + return getDatacenterLoadMatrix(DATACENTER_PROXY_MATRIX); + } + + @Override + public List getDatacenterVMLoadMatrix() { + return getDatacenterLoadMatrix(DATACENTER_VM_MATRIX); + } + + @Override + public List getDatacenterSessionLoadMatrix() { + return getDatacenterLoadMatrix(DATACENTER_ACTIVE_SESSION_MATRIX); + } + + @Override + public List> getProxyLoadMatrix() { + ArrayList> l = new ArrayList>(); + + Transaction txn = Transaction.currentTxn();; + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(PROXY_ASSIGNMENT_MATRIX); + ResultSet rs = pstmt.executeQuery(); + while(rs.next()) { + l.add(new Pair(rs.getLong(1), rs.getInt(2))); + } + } catch (SQLException e) { + } catch (Throwable e) { + } + return l; + } + + @Override + public List> getDatacenterStoragePoolHostInfo(long dcId, boolean countAllPoolTypes) { + ArrayList> l = new ArrayList>(); + + Transaction txn = Transaction.currentTxn();; + PreparedStatement pstmt = null; + try { + if(countAllPoolTypes) + pstmt = txn.prepareAutoCloseStatement(STORAGE_POOL_HOST_INFO); + else + pstmt = txn.prepareAutoCloseStatement(SHARED_STORAGE_POOL_HOST_INFO); + pstmt.setLong(1, dcId); + + ResultSet rs = pstmt.executeQuery(); + while(rs.next()) { + l.add(new Pair(rs.getLong(1), rs.getInt(2))); + } + } catch (SQLException e) { + } catch (Throwable e) { + } + return l; + } + + @Override + public int getProxyStaticLoad(long proxyVmId) { + Transaction txn = Transaction.currentTxn();; + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(GET_PROXY_LOAD); + pstmt.setLong(1, proxyVmId); + + ResultSet rs = pstmt.executeQuery(); + if(rs != null && rs.first()) + return rs.getInt(1); + } catch (SQLException e) { + } catch (Throwable e) { + } + return 0; + } + + @Override + public int getProxyActiveLoad(long proxyVmId) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(GET_PROXY_ACTIVE_LOAD); + pstmt.setLong(1, proxyVmId); + + ResultSet rs = pstmt.executeQuery(); + if(rs != null && rs.first()) + return rs.getInt(1); + } catch (SQLException e) { + } catch (Throwable e) { + } + return 0; + } + + private List getDatacenterLoadMatrix(String sql) { + ArrayList l = new ArrayList(); + + Transaction txn = Transaction.currentTxn();; + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(sql); + ResultSet rs = pstmt.executeQuery(); + while(rs.next()) { + ConsoleProxyLoadInfo info = new ConsoleProxyLoadInfo(); + info.setId(rs.getLong(1)); + info.setName(rs.getString(2)); + info.setCount(rs.getInt(3)); + l.add(info); + } + } catch (SQLException e) { + } catch (Throwable e) { + } + return l; + } + + @Override + public List getRunningProxyListByMsid(long msid) { + List l = new ArrayList(); + Transaction txn = Transaction.currentTxn();; + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement( + "SELECT c.id FROM console_proxy c, vm_instance v, host h " + + "WHERE c.id=v.id AND v.state='Running' AND v.host_id=h.id AND h.mgmt_server_id=?"); + + pstmt.setLong(1, msid); + ResultSet rs = pstmt.executeQuery(); + while(rs.next()) { + l.add(rs.getLong(1)); + } + } catch (SQLException e) { + } catch (Throwable e) { + } + return l; + } +} diff --git a/core/src/com/cloud/vm/dao/DomainRouterDao.java b/core/src/com/cloud/vm/dao/DomainRouterDao.java new file mode 100755 index 00000000000..837e580cfe0 --- /dev/null +++ b/core/src/com/cloud/vm/dao/DomainRouterDao.java @@ -0,0 +1,117 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm.dao; + +import java.util.List; + +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.DomainRouterVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.DomainRouter.Role; + +/** + * + * DomainRouterDao implements + */ +public interface DomainRouterDao extends GenericDao { + //@Deprecated + //public boolean updateIf(DomainRouterVO router, State state, State... ifStates); + + /** + * gets the DomainRouterVO by user id and data center + * @Param dcId data center Id. + * @return list of DomainRouterVO + */ + public List listByDataCenter(long dcId); + + /** + * gets the DomainRouterVO by account id and data center + * @param account id of the user. + * @Param dcId data center Id. + * @return DomainRouterVO + */ + public DomainRouterVO findBy(long accountId, long dcId); + + /** + * gets the DomainRouterVO by user id. + * @param userId id of the user. + * @Param dcId data center Id. + * @return list of DomainRouterVO + */ + public List listBy(long userId); + + /** + * Update the domainrouterVO only if the state is correct and the hostId is set. + * + * @param router router object + * @param event event that forces this update + * @param hostId host id to set to. + * @return true if update worked; false if not. + */ + public boolean updateIf(DomainRouterVO router, VirtualMachine.Event event, Long hostId); + + /** + * list virtual machine routers by host id. pass in null to get all + * virtual machine routers. + * @param hostId id of the host. null if to get all. + * @return list of DomainRouterVO + */ + public List listByHostId(Long hostId); + + /** + * list virtual machine routers by host id. exclude destroyed, stopped, expunging VM, + * pass in null to get all + * virtual machine routers. + * @param hostId id of the host. null if to get all. + * @return list of DomainRouterVO + */ + public List listUpByHostId(Long hostId); + + /** + * Finds a domain router based on the ip address it is assigned to. + * @param ipAddress + * @return DomainRouterVO or null if not found. + */ + public DomainRouterVO findByPublicIpAddress(String ipAddress); + + + public List findLonelyRouters(); + + /** + * Gets the next dhcp ip address to be used for vms from this domain router. + * @param id domain router id + * @return next ip address + */ + long getNextDhcpIpAddress(long id); + + /** + * Find the list of domain routers for a domain + * @param id + * @return + */ + public List listByDomain(Long id); + + /** + * Find the list of domain routers on a vlan + * @param id the id of the vlan record in the vlan table + * @return + */ + public List listByVlanDbId(Long vlanId); + + DomainRouterVO findBy(long accountId, long dcId, Role role); +} diff --git a/core/src/com/cloud/vm/dao/DomainRouterDaoImpl.java b/core/src/com/cloud/vm/dao/DomainRouterDaoImpl.java new file mode 100755 index 00000000000..2a12035ded4 --- /dev/null +++ b/core/src/com/cloud/vm/dao/DomainRouterDaoImpl.java @@ -0,0 +1,299 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.UpdateBuilder; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.DomainRouterVO; +import com.cloud.vm.State; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.DomainRouter.Role; + +@Local(value = { DomainRouterDao.class }) +public class DomainRouterDaoImpl extends GenericDaoBase implements DomainRouterDao { + private static final Logger s_logger = Logger.getLogger(DomainRouterDaoImpl.class); + + private static final String FindLonelyRoutersSql = "SELECT dr.id FROM domain_router dr, vm_instance vm WHERE dr.id=vm.id AND vm.state = 'Running' AND dr.id NOT IN (SELECT DISTINCT domain_router_id FROM user_vm uvm, vm_instance vmi WHERE (vmi.state = 'Running' OR vmi.state = 'Starting' OR vmi.state='Stopping' OR vmi.state = 'Migrating') AND vmi.id = uvm.id)"; + private static final String GetNextDhcpAddressSql = "UPDATE domain_router set dhcp_ip_address = (@LAST_DHCP:=dhcp_ip_address) + 1 WHERE id = ?"; + private static final String GetLastDhcpSql = "SELECT @LAST_DHCP"; + + protected final SearchBuilder IdStatesSearch; + protected final SearchBuilder AccountDcSearch; + protected final SearchBuilder AccountDcRoleSearch; + + protected final SearchBuilder AccountSearch; + protected final SearchBuilder DcSearch; + protected final SearchBuilder IpSearch; + protected final SearchBuilder HostSearch; + protected final SearchBuilder HostUpSearch; + protected final SearchBuilder DomainIdSearch; + protected final SearchBuilder VlanDbIdSearch; + protected final SearchBuilder StateChangeSearch; + protected final Attribute _updateTimeAttr; + + protected DomainRouterDaoImpl() { + DcSearch = createSearchBuilder(); + DcSearch.and("dc", DcSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DcSearch.done(); + + IdStatesSearch = createSearchBuilder(); + IdStatesSearch.and("id", IdStatesSearch.entity().getId(), SearchCriteria.Op.EQ); + IdStatesSearch.and("states", IdStatesSearch.entity().getState(), SearchCriteria.Op.IN); + IdStatesSearch.done(); + + AccountDcSearch = createSearchBuilder(); + AccountDcSearch.and("account", AccountDcSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountDcSearch.and("dc", AccountDcSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + AccountDcSearch.done(); + + AccountDcRoleSearch = createSearchBuilder(); + AccountDcRoleSearch.and("account", AccountDcRoleSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountDcRoleSearch.and("dc", AccountDcRoleSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + AccountDcRoleSearch.and("role", AccountDcRoleSearch.entity().getRole(), SearchCriteria.Op.EQ); + AccountDcRoleSearch.done(); + + AccountSearch = createSearchBuilder(); + AccountSearch.and("account", AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountSearch.done(); + + IpSearch = createSearchBuilder(); + IpSearch.and("ip", IpSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); + IpSearch.done(); + + HostSearch = createSearchBuilder(); + HostSearch.and("host", HostSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostSearch.done(); + + HostUpSearch = createSearchBuilder(); + HostUpSearch.and("host", HostUpSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostUpSearch.and("states", HostUpSearch.entity().getState(), SearchCriteria.Op.NIN); + HostUpSearch.done(); + + DomainIdSearch = createSearchBuilder(); + DomainIdSearch.and("domainId", DomainIdSearch.entity().getDomainId(), SearchCriteria.Op.EQ); + DomainIdSearch.done(); + + VlanDbIdSearch = createSearchBuilder(); + VlanDbIdSearch.and("vlanDbId", VlanDbIdSearch.entity().getVlanDbId(), SearchCriteria.Op.EQ); + VlanDbIdSearch.done(); + + StateChangeSearch = createSearchBuilder(); + StateChangeSearch.and("id", StateChangeSearch.entity().getId(), SearchCriteria.Op.EQ); + StateChangeSearch.and("states", StateChangeSearch.entity().getState(), SearchCriteria.Op.EQ); + StateChangeSearch.and("host", StateChangeSearch.entity().getHostId(), SearchCriteria.Op.EQ); + StateChangeSearch.and("update", StateChangeSearch.entity().getUpdated(), SearchCriteria.Op.EQ); + StateChangeSearch.done(); + + _updateTimeAttr = _allAttributes.get("updateTime"); + assert _updateTimeAttr != null : "Couldn't get this updateTime attribute"; + } + + @Override + public DomainRouterVO findByPublicIpAddress(String ipAddress) { + SearchCriteria sc = IpSearch.create(); + sc.setParameters("ip", ipAddress); + return findOneActiveBy(sc); + } + + @Override + public boolean remove(Long id) { + Transaction txn = Transaction.currentTxn(); + txn.start(); + DomainRouterVO router = createForUpdate(); + router.setPublicIpAddress(null); + UpdateBuilder ub = getUpdateBuilder(router); + ub.set(router, "state", State.Destroyed); + update(id, ub); + + boolean result = super.remove(id); + txn.commit(); + return result; + } + + @Override + public boolean updateIf(DomainRouterVO router, VirtualMachine.Event event, Long hostId) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("updateIf called on " + router.toString() + " event " + event.toString() + " host " + hostId); + } + State oldState = router.getState(); + State newState = oldState.getNextState(event); + long oldDate = router.getUpdated(); + + Long oldHostId = router.getHostId(); + + if (newState == null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("There's no way to transition from old state: " + oldState.toString() + " event: " + event.toString()); + } + return false; + } + + SearchCriteria sc = StateChangeSearch.create(); + sc.setParameters("id", router.getId()); + sc.setParameters("states", oldState); + sc.setParameters("host", router.getHostId()); + sc.setParameters("update", router.getUpdated()); + + router.incrUpdated(); + UpdateBuilder ub = getUpdateBuilder(router); + if(newState == State.Running) { + // save current running host id + ub.set(router, "lastHostId", router.getHostId()); + } + + ub.set(router, "state", newState); + ub.set(router, "hostId", hostId); + ub.set(router, _updateTimeAttr, new Date()); + + int result = update(router, sc); + if (result == 0 && s_logger.isDebugEnabled()) { + DomainRouterVO vo = findById(router.getId()); + StringBuilder str = new StringBuilder("Unable to update ").append(vo.toString()); + str.append(": DB Data={Host=").append(vo.getHostId()).append("; State=").append(vo.getState().toString()).append("; updated=").append( + vo.getUpdated()); + str.append("} New Data: {Host=").append(router.getHostId()).append("; State=").append(router.getState().toString()).append("; updated=").append( + router.getUpdated()); + str.append("} Stale Data: {Host=").append(oldHostId).append("; State=").append(oldState.toString()).append("; updated=").append(oldDate) + .append("}"); + s_logger.debug(str.toString()); + } + + return result > 0; + } + + @Override + public List listByDataCenter(long dcId) { + SearchCriteria sc = DcSearch.create(); + sc.setParameters("dc", dcId); + return listActiveBy(sc); + } + + @Override + public DomainRouterVO findBy(long accountId, long dcId) { + SearchCriteria sc = AccountDcRoleSearch.create(); + sc.setParameters("account", accountId); + sc.setParameters("dc", dcId); + sc.setParameters("role", Role.DHCP_FIREWALL_LB_PASSWD_USERDATA); + return findOneActiveBy(sc); + } + + @Override + public DomainRouterVO findBy(long accountId, long dcId, Role role) { + SearchCriteria sc = AccountDcRoleSearch.create(); + sc.setParameters("account", accountId); + sc.setParameters("dc", dcId); + sc.setParameters("role", role); + return findOneActiveBy(sc); + } + + @Override + public List listBy(long accountId) { + SearchCriteria sc = AccountSearch.create(); + sc.setParameters("account", accountId); + return listActiveBy(sc); + } + + @Override + public List listByHostId(Long hostId) { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("host", hostId); + return listActiveBy(sc); + } + + @Override + public List listUpByHostId(Long hostId) { + SearchCriteria sc = HostUpSearch.create(); + if(hostId != null){ + sc.setParameters("host", hostId); + } + sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging}); + return listActiveBy(sc); + } + + @Override + public List findLonelyRouters() { + ArrayList ids = new ArrayList(); + PreparedStatement pstmt = null; + Transaction txn = Transaction.currentTxn(); + try { + pstmt = txn.prepareAutoCloseStatement(FindLonelyRoutersSql); + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + ids.add(rs.getLong(1)); + } + + } catch (SQLException e) { + throw new CloudRuntimeException("Problem finding routers: " + pstmt.toString(), e); + } + return ids; + } + + @Override + public long getNextDhcpIpAddress(long id) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement(GetNextDhcpAddressSql); + pstmt.setLong(1, id); + pstmt.executeUpdate(); + + pstmt = txn.prepareAutoCloseStatement(GetLastDhcpSql); + ResultSet rs = pstmt.executeQuery(); + if (rs == null || !rs.next()) { + throw new CloudRuntimeException("Unable to fetch a sequence with " + pstmt.toString()); + } + + long result = rs.getLong(1); + return result; + } catch (SQLException e) { + txn.rollback(); + s_logger.warn("DB Exception", e); + throw new CloudRuntimeException("DB Exception on " + pstmt.toString(), e); + } + } + + @Override + public List listByDomain(Long domainId) { + SearchCriteria sc = DomainIdSearch.create(); + sc.setParameters("domainId", domainId); + return listBy(sc); + } + + @Override + public List listByVlanDbId(Long vlanDbId) { + SearchCriteria sc = VlanDbIdSearch.create(); + sc.setParameters("vlanDbId", vlanDbId); + return listBy(sc); + } +} diff --git a/core/src/com/cloud/vm/dao/SecondaryStorageVmDao.java b/core/src/com/cloud/vm/dao/SecondaryStorageVmDao.java new file mode 100644 index 00000000000..5c6cb8e3399 --- /dev/null +++ b/core/src/com/cloud/vm/dao/SecondaryStorageVmDao.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm.dao; + +import java.util.List; + + +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.SecondaryStorageVmVO; +import com.cloud.vm.State; +import com.cloud.vm.VirtualMachine; + +public interface SecondaryStorageVmDao extends GenericDao { + + public List getSecStorageVmListInStates(long dataCenterId, State... states); + public List getSecStorageVmListInStates(State... states); + + public List listByHostId(long hostId); + + public List listUpByHostId(long hostId); + + public List listByZoneId(long zoneId); + + + public List getRunningSecStorageVmListByMsid(long msid); + + public boolean updateIf(SecondaryStorageVmVO vm, VirtualMachine.Event event, Long hostId); +} diff --git a/core/src/com/cloud/vm/dao/SecondaryStorageVmDaoImpl.java b/core/src/com/cloud/vm/dao/SecondaryStorageVmDaoImpl.java new file mode 100644 index 00000000000..2005b324888 --- /dev/null +++ b/core/src/com/cloud/vm/dao/SecondaryStorageVmDaoImpl.java @@ -0,0 +1,213 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.UpdateBuilder; +import com.cloud.vm.SecondaryStorageVmVO; +import com.cloud.vm.State; +import com.cloud.vm.VirtualMachine; + +@Local(value={SecondaryStorageVmDao.class}) +public class SecondaryStorageVmDaoImpl extends GenericDaoBase implements SecondaryStorageVmDao { + private static final Logger s_logger = Logger.getLogger(SecondaryStorageVmDaoImpl.class); + + protected SearchBuilder DataCenterStatusSearch; + protected SearchBuilder StateSearch; + protected SearchBuilder HostSearch; + protected SearchBuilder HostUpSearch; + protected SearchBuilder ZoneSearch; + protected SearchBuilder StateChangeSearch; + + protected final Attribute _updateTimeAttr; + + public SecondaryStorageVmDaoImpl() { + DataCenterStatusSearch = createSearchBuilder(); + DataCenterStatusSearch.and("dc", DataCenterStatusSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + DataCenterStatusSearch.and("states", DataCenterStatusSearch.entity().getState(), SearchCriteria.Op.IN); + DataCenterStatusSearch.done(); + + StateSearch = createSearchBuilder(); + StateSearch.and("states", StateSearch.entity().getState(), SearchCriteria.Op.IN); + StateSearch.done(); + + HostSearch = createSearchBuilder(); + HostSearch.and("host", HostSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostSearch.done(); + + HostUpSearch = createSearchBuilder(); + HostUpSearch.and("host", HostUpSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostUpSearch.and("states", HostUpSearch.entity().getState(), SearchCriteria.Op.NIN); + HostUpSearch.done(); + + ZoneSearch = createSearchBuilder(); + ZoneSearch.and("zone", ZoneSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + ZoneSearch.done(); + + StateChangeSearch = createSearchBuilder(); + StateChangeSearch.and("id", StateChangeSearch.entity().getId(), SearchCriteria.Op.EQ); + StateChangeSearch.and("states", StateChangeSearch.entity().getState(), SearchCriteria.Op.EQ); + StateChangeSearch.and("host", StateChangeSearch.entity().getHostId(), SearchCriteria.Op.EQ); + StateChangeSearch.and("update", StateChangeSearch.entity().getUpdated(), SearchCriteria.Op.EQ); + StateChangeSearch.done(); + + _updateTimeAttr = _allAttributes.get("updateTime"); + assert _updateTimeAttr != null : "Couldn't get this updateTime attribute"; + } + + @Override + public boolean updateIf(SecondaryStorageVmVO vm, VirtualMachine.Event event, Long hostId) { + State oldState = vm.getState(); + State newState = oldState.getNextState(event); + + Long oldHostId = vm.getHostId(); + long oldDate = vm.getUpdated(); + + if (newState == null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("There's no way to transition from old state: " + oldState.toString() + " event: " + event.toString()); + } + return false; + } + + SearchCriteria sc = StateChangeSearch.create(); + sc.setParameters("id", vm.getId()); + sc.setParameters("states", oldState); + sc.setParameters("host", vm.getHostId()); + sc.setParameters("update", vm.getUpdated()); + + vm.incrUpdated(); + UpdateBuilder ub = getUpdateBuilder(vm); + if(newState == State.Running) { + // save current running host id + ub.set(vm, "lastHostId", vm.getHostId()); + } + + ub.set(vm, _updateTimeAttr, new Date()); + ub.set(vm, "state", newState); + ub.set(vm, "hostId", hostId); + if (newState == State.Stopped) { + ub.set(vm, "hostId", null); + } + + int result = update(vm, sc); + + if (result == 0 && s_logger.isDebugEnabled()) { + SecondaryStorageVmVO vo = findById(vm.getId()); + StringBuilder str = new StringBuilder("Unable to update ").append(vo.toString()); + str.append(": DB Data={Host=").append(vo.getHostId()).append("; State=").append(vo.getState().toString()).append("; updated=").append(vo.getUpdated()); + str.append("} New Data: {Host=").append(vm.getHostId()).append("; State=").append(vm.getState().toString()).append("; updated=").append(vm.getUpdated()); + str.append("} Stale Data: {Host=").append(oldHostId).append("; State=").append(oldState.toString()).append("; updated=").append(oldDate).append("}"); + s_logger.debug(str.toString()); + } + + return result > 0; + } + + + @Override + public boolean remove(Long id) { + Transaction txn = Transaction.currentTxn(); + txn.start(); + SecondaryStorageVmVO proxy = createForUpdate(); + proxy.setPublicIpAddress(null); + proxy.setPrivateIpAddress(null); + + UpdateBuilder ub = getUpdateBuilder(proxy); + ub.set(proxy, "state", State.Destroyed); + ub.set(proxy, "privateIpAddress", null); + update(id, ub); + + boolean result = super.remove(id); + txn.commit(); + return result; + } + + @Override + public List getSecStorageVmListInStates(long dataCenterId, State... states) { + SearchCriteria sc = DataCenterStatusSearch.create(); + sc.setParameters("states", (Object[])states); + sc.setParameters("dc", dataCenterId); + return listActiveBy(sc); + } + + @Override + public List getSecStorageVmListInStates(State... states) { + SearchCriteria sc = StateSearch.create(); + sc.setParameters("states", (Object[])states); + return listActiveBy(sc); + } + + @Override + public List listByHostId(long hostId) { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("host", hostId); + return listActiveBy(sc); + } + + @Override + public List listUpByHostId(long hostId) { + SearchCriteria sc = HostUpSearch.create(); + sc.setParameters("host", hostId); + sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging}); + return listActiveBy(sc); + } + + @Override + public List getRunningSecStorageVmListByMsid(long msid) { + List l = new ArrayList(); + Transaction txn = Transaction.currentTxn();; + PreparedStatement pstmt = null; + try { + pstmt = txn.prepareAutoCloseStatement( + "SELECT s.id FROM secondary_storage_vm s, vm_instance v, host h " + + "WHERE s.id=v.id AND v.state='Running' AND v.host_id=h.id AND h.mgmt_server_id=?"); + + pstmt.setLong(1, msid); + ResultSet rs = pstmt.executeQuery(); + while(rs.next()) { + l.add(rs.getLong(1)); + } + } catch (SQLException e) { + } catch (Throwable e) { + } + return l; + } + + @Override + public List listByZoneId(long zoneId) { + SearchCriteria sc = ZoneSearch.create(); + sc.setParameters("zone", zoneId); + return listActiveBy(sc); + } +} diff --git a/core/src/com/cloud/vm/dao/UserVmDao.java b/core/src/com/cloud/vm/dao/UserVmDao.java new file mode 100755 index 00000000000..72faff705a2 --- /dev/null +++ b/core/src/com/cloud/vm/dao/UserVmDao.java @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.State; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; + +public interface UserVmDao extends GenericDao { + List listByAccountId(long id); + + List listByAccountAndPod(long accountId, long podId); + List listByAccountAndDataCenter(long accountId, long dcId); + List listByHostId(Long hostId); + List listUpByHostId(Long hostId); + + /** + * Find vms under the same router in the state. + * @param routerId id of the router. + * @param state state that it's in. + * @return list of userVmVO + */ + List listBy(long routerId, State... state); + + UserVmVO findByName(String name); + + /** + * This method is of supreme importance in the management of VMs. It updates a uservm if and only if + * the following condition are true. If the update is complete, all changes to the uservm entity + * are persisted. The state is also changed to the new state. + * + * 1. There's a transition from the current state via the event to a new state. + * 2. The db has not changed on the current state, update time, and host id sent. + * + * @param vm vm object to persist. + * @param event + * @param hostId + * @return true if updated, false if not. + */ + boolean updateIf(UserVmVO vm, VirtualMachine.Event event, Long hostId); + + /** + * Updates display name and group for vm; enables/disables ha + * @param id vm id. + * @param displan name, group and enable for ha + */ + void updateVM(long id, String displayName, String group, boolean enable); + + List findDestroyedVms(Date date); + + /** + * Find all vms that use a domain router + * @param routerId + * @return + */ + List listByRouterId(long routerId); + + /** + * List running VMs on the specified host + * @param id + * @return + */ + public List listRunningByHostId(long hostId); + + /** + * List user vm instances with virtualized networking (i.e. not direct attached networking) for the given account and datacenter + * @param accountId will search for vm instances belonging to this account + * @param dcId will search for vm instances in this zone + * @return the list of vm instances owned by the account in the given data center that have virtualized networking (not direct attached networking) + */ + List listVirtualNetworkInstancesByAcctAndZone(long accountId, long dcId); + + List listVmsUsingGuestIpAddress(long dcId, String ipAddress); +} diff --git a/core/src/com/cloud/vm/dao/UserVmDaoImpl.java b/core/src/com/cloud/vm/dao/UserVmDaoImpl.java new file mode 100755 index 00000000000..6e75bc179bb --- /dev/null +++ b/core/src/com/cloud/vm/dao/UserVmDaoImpl.java @@ -0,0 +1,296 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.cloud.vm.dao; + +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.offering.ServiceOffering; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.utils.component.ComponentLocator; +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.UpdateBuilder; +import com.cloud.vm.State; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; + +@Local(value={UserVmDao.class}) +public class UserVmDaoImpl extends GenericDaoBase implements UserVmDao { + public static final Logger s_logger = Logger.getLogger(UserVmDaoImpl.class.getName()); + + protected final SearchBuilder RouterStateSearch; + protected final SearchBuilder RouterIdSearch; + protected final SearchBuilder AccountPodSearch; + protected final SearchBuilder AccountDataCenterSearch; + protected final SearchBuilder AccountSearch; + protected final SearchBuilder HostSearch; + protected final SearchBuilder HostUpSearch; + protected final SearchBuilder HostRunningSearch; + protected final SearchBuilder NameSearch; + protected final SearchBuilder StateChangeSearch; + protected final SearchBuilder GuestIpSearch; + + protected final SearchBuilder DestroySearch; + protected SearchBuilder AccountDataCenterVirtualSearch; + protected final Attribute _updateTimeAttr; + + protected UserVmDaoImpl() { + AccountSearch = createSearchBuilder(); + AccountSearch.and("account", AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountSearch.done(); + + HostSearch = createSearchBuilder(); + HostSearch.and("host", HostSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostSearch.done(); + + HostUpSearch = createSearchBuilder(); + HostUpSearch.and("host", HostUpSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostUpSearch.and("states", HostUpSearch.entity().getState(), SearchCriteria.Op.NIN); + HostUpSearch.done(); + + HostRunningSearch = createSearchBuilder(); + HostRunningSearch.and("host", HostRunningSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostRunningSearch.and("state", HostRunningSearch.entity().getState(), SearchCriteria.Op.EQ); + HostRunningSearch.done(); + + NameSearch = createSearchBuilder(); + NameSearch.and("name", NameSearch.entity().getName(), SearchCriteria.Op.EQ); + NameSearch.done(); + + RouterStateSearch = createSearchBuilder(); + RouterStateSearch.and("router", RouterStateSearch.entity().getDomainRouterId(), SearchCriteria.Op.EQ); + RouterStateSearch.done(); + + RouterIdSearch = createSearchBuilder(); + RouterIdSearch.and("router", RouterIdSearch.entity().getDomainRouterId(), SearchCriteria.Op.EQ); + RouterIdSearch.done(); + + AccountPodSearch = createSearchBuilder(); + AccountPodSearch.and("account", AccountPodSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountPodSearch.and("pod", AccountPodSearch.entity().getPodId(), SearchCriteria.Op.EQ); + AccountPodSearch.done(); + + AccountDataCenterSearch = createSearchBuilder(); + AccountDataCenterSearch.and("account", AccountDataCenterSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountDataCenterSearch.and("dc", AccountDataCenterSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + AccountDataCenterSearch.done(); + + StateChangeSearch = createSearchBuilder(); + StateChangeSearch.and("id", StateChangeSearch.entity().getId(), SearchCriteria.Op.EQ); + StateChangeSearch.and("states", StateChangeSearch.entity().getState(), SearchCriteria.Op.EQ); + StateChangeSearch.and("host", StateChangeSearch.entity().getHostId(), SearchCriteria.Op.EQ); + StateChangeSearch.and("update", StateChangeSearch.entity().getUpdated(), SearchCriteria.Op.EQ); + StateChangeSearch.done(); + + GuestIpSearch = createSearchBuilder(); + GuestIpSearch.and("dc", GuestIpSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + GuestIpSearch.and("ip", GuestIpSearch.entity().getGuestIpAddress(), SearchCriteria.Op.EQ); + GuestIpSearch.and("states", GuestIpSearch.entity().getState(), SearchCriteria.Op.NIN); + GuestIpSearch.done(); + + DestroySearch = createSearchBuilder(); + DestroySearch.and("state", DestroySearch.entity().getState(), SearchCriteria.Op.IN); + DestroySearch.and("updateTime", DestroySearch.entity().getUpdateTime(), SearchCriteria.Op.LT); + DestroySearch.done(); + + _updateTimeAttr = _allAttributes.get("updateTime"); + assert _updateTimeAttr != null : "Couldn't get this updateTime attribute"; + } + + public List listByAccountAndPod(long accountId, long podId) { + SearchCriteria sc = AccountPodSearch.create(); + sc.setParameters("account", accountId); + sc.setParameters("pod", podId); + + return listBy(sc); + } + + public List listByAccountAndDataCenter(long accountId, long dcId) { + SearchCriteria sc = AccountDataCenterSearch.create(); + sc.setParameters("account", accountId); + sc.setParameters("dc", dcId); + + return listBy(sc); + } + + @Override + public List listBy(long routerId, State... states) { + SearchCriteria sc = RouterStateSearch.create(); + SearchCriteria ssc = createSearchCriteria(); + + sc.setParameters("router", routerId); + for (State state: states) { + ssc.addOr("state", SearchCriteria.Op.EQ, state.toString()); + } + sc.addAnd("state", SearchCriteria.Op.SC, ssc); + return listBy(sc); + } + + @Override + public void updateVM(long id, String displayName, String group, boolean enable) { + UserVmVO vo = createForUpdate(); + vo.setDisplayName(displayName); + vo.setGroup(group); + vo.setHaEnabled(enable); + update(id, vo); + } + + @Override + public List listByRouterId(long routerId) { + SearchCriteria sc = RouterIdSearch.create(); + + sc.setParameters("router", routerId); + + return listBy(sc); + } + + @Override + public boolean updateIf(UserVmVO vm, VirtualMachine.Event event, Long hostId) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("UpdateIf called " + vm.toString() + " event " + event.toString() + " host " + hostId); + } + + State oldState = vm.getState(); + State newState = oldState.getNextState(event); + Long oldHostId = vm.getHostId(); + long oldDate = vm.getUpdated(); + + + if (newState == null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("There's no way to transition from old state: " + oldState.toString() + " event: " + event.toString()); + } + return false; + } + + SearchCriteria sc = StateChangeSearch.create(); + sc.setParameters("id", vm.getId()); + sc.setParameters("states", oldState); + sc.setParameters("host", vm.getHostId()); + sc.setParameters("update", vm.getUpdated()); + + vm.incrUpdated(); + UpdateBuilder ub = getUpdateBuilder(vm); + if(newState == State.Running) { + // save current running host id to last_host_id field + ub.set(vm, "lastHostId", vm.getHostId()); + } else if(newState == State.Expunging) { + ub.set(vm, "lastHostId", null); + } + + ub.set(vm, "state", newState); + ub.set(vm, "hostId", hostId); + ub.set(vm, _updateTimeAttr, new Date()); + + int result = update(vm, sc); + if (result == 0 && s_logger.isDebugEnabled()) { + UserVmVO vo = findById(vm.getId()); + StringBuilder str = new StringBuilder("Unable to update ").append(vo.toString()); + str.append(": DB Data={Host=").append(vo.getHostId()).append("; State=").append(vo.getState().toString()).append("; updated=").append(vo.getUpdated()); + str.append("} New Data: {Host=").append(vm.getHostId()).append("; State=").append(vm.getState().toString()).append("; updated=").append(vm.getUpdated()); + str.append("} Stale Data: {Host=").append(oldHostId).append("; State=").append(oldState.toString()).append("; updated=").append(oldDate).append("}"); + s_logger.debug(str.toString()); + } + + return result > 0; + } + + @Override + public List findDestroyedVms(Date date) { + SearchCriteria sc = DestroySearch.create(); + sc.setParameters("state", State.Destroyed, State.Expunging); + sc.setParameters("updateTime", date); + + return listActiveBy(sc); + } + + public List listByAccountId(long id) { + SearchCriteria sc = AccountSearch.create(); + sc.setParameters("account", id); + return listActiveBy(sc); + } + + public List listByHostId(Long id) { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("host", id); + + return listActiveBy(sc); + } + + @Override + public List listUpByHostId(Long hostId) { + SearchCriteria sc = HostUpSearch.create(); + sc.setParameters("host", hostId); + sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging}); + return listActiveBy(sc); + } + + public List listRunningByHostId(long hostId) { + SearchCriteria sc = HostRunningSearch.create(); + sc.setParameters("host", hostId); + sc.setParameters("state", State.Running); + + return listActiveBy(sc); + } + + public UserVmVO findByName(String name) { + SearchCriteria sc = NameSearch.create(); + sc.setParameters("name", name); + return findOneBy(sc); + } + + @Override + public List listVirtualNetworkInstancesByAcctAndZone(long accountId, long dcId) { + if (AccountDataCenterVirtualSearch == null) { + ServiceOfferingDao offeringDao = ComponentLocator.getLocator("management-server").getDao(ServiceOfferingDao.class); + SearchBuilder offeringSearch = offeringDao.createSearchBuilder(); + offeringSearch.and("guestIpType", offeringSearch.entity().getGuestIpType(), SearchCriteria.Op.EQ); + + AccountDataCenterVirtualSearch = createSearchBuilder(); + AccountDataCenterVirtualSearch.and("account", AccountDataCenterVirtualSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountDataCenterVirtualSearch.and("dc", AccountDataCenterVirtualSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + AccountDataCenterVirtualSearch.join("offeringSearch", offeringSearch, AccountDataCenterVirtualSearch.entity().getServiceOfferingId(), offeringSearch.entity().getId()); + AccountDataCenterVirtualSearch.done(); + } + + SearchCriteria sc = AccountDataCenterVirtualSearch.create(); + sc.setParameters("account", accountId); + sc.setParameters("dc", dcId); + sc.setJoinParameters("offeringSearch", "guestIpType", ServiceOffering.GuestIpType.Virtualized); + + return listActiveBy(sc); + } + + @Override + public List listVmsUsingGuestIpAddress(long dcId, String ipAddress) { + SearchCriteria sc = GuestIpSearch.create(); + sc.setParameters("dc", dcId); + sc.setParameters("ip", ipAddress); + sc.setParameters("states", new Object[] {State.Destroyed, State.Expunging}); + + return listActiveBy(sc); + } +} diff --git a/core/src/com/cloud/vm/dao/VMInstanceDao.java b/core/src/com/cloud/vm/dao/VMInstanceDao.java new file mode 100644 index 00000000000..653445ad331 --- /dev/null +++ b/core/src/com/cloud/vm/dao/VMInstanceDao.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.vm.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.State; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; + +/* + * Data Access Object for vm_instance table + */ +public interface VMInstanceDao extends GenericDao { + /** + * What are the vms running on this host? + * @param hostId host. + * @return list of VMInstanceVO running on that host. + */ + List listByHostId(long hostId); + List listByLastHostId(long hostId); + + /** + * List VMs by zone ID + * @param zoneId + * @return list of VMInstanceVO in the specified zone + */ + List listByZoneId(long zoneId); + + /** + * Lists non-expunged VMs by zone ID and templateId + * @param zoneId + * @return list of VMInstanceVO in the specified zone, deployed from the specified template, that are not expunged + */ + public List listNonExpungedByZoneAndTemplate(long zoneId, long templateId); + + boolean updateIf(VMInstanceVO vm, VirtualMachine.Event event, Long hostId); + + /** + * Find vm instance with names like. + * + * @param name name that fits SQL like. + * @return list of VMInstanceVO + */ + List findVMInstancesLike(String name); + + List findVMInTransition(Date time, State... states); + + /** + * return the counts of domain routers and console proxies running on the host + * @param hostId + * @return + */ + Integer[] countRoutersAndProxies(Long hostId); + + List listByTypes(VirtualMachine.Type... types); + + VMInstanceVO findByIdTypes(long id, VirtualMachine.Type... types); + + void updateProxyId(long id, Long proxyId, Date time); + + List listByHostIdTypes(long hostid, VirtualMachine.Type... types); + + List listUpByHostIdTypes(long hostid, VirtualMachine.Type... types); +} diff --git a/core/src/com/cloud/vm/dao/VMInstanceDaoImpl.java b/core/src/com/cloud/vm/dao/VMInstanceDaoImpl.java new file mode 100644 index 00000000000..3ec6b2a12dc --- /dev/null +++ b/core/src/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -0,0 +1,278 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.vm.dao; + + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.Date; +import java.util.List; + +import javax.ejb.Local; + +import org.apache.log4j.Logger; + +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.UpdateBuilder; +import com.cloud.vm.State; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachine.Type; + +@Local(value = { VMInstanceDao.class }) +public class VMInstanceDaoImpl extends GenericDaoBase implements VMInstanceDao { + + public static final Logger s_logger = Logger.getLogger(VMInstanceDaoImpl.class.getName()); + + private static final String COUNT_ROUTERS_AND_PROXIES = "SELECT count(*) from `cloud`.`vm_instance` where host_id = ? AND type = 'DomainRouter'" + " UNION ALL" + + " SELECT count(*) from `cloud`.`vm_instance` where host_id = ? AND type = 'ConsoleProxy'"; + + protected final SearchBuilder IdStatesSearch; + protected final SearchBuilder HostSearch; + protected final SearchBuilder LastHostSearch; + protected final SearchBuilder ZoneSearch; + protected final SearchBuilder ZoneTemplateNonExpungedSearch; + protected final SearchBuilder NameLikeSearch; + protected final SearchBuilder StateChangeSearch; + protected final SearchBuilder TransitionSearch; + protected final SearchBuilder TypesSearch; + protected final SearchBuilder IdTypesSearch; + protected final SearchBuilder HostIdTypesSearch; + protected final SearchBuilder HostIdUpTypesSearch; + + protected final Attribute _updateTimeAttr; + + protected VMInstanceDaoImpl() { + IdStatesSearch = createSearchBuilder(); + IdStatesSearch.and("id", IdStatesSearch.entity().getId(), SearchCriteria.Op.EQ); + IdStatesSearch.and("states", IdStatesSearch.entity().getState(), SearchCriteria.Op.IN); + IdStatesSearch.done(); + + HostSearch = createSearchBuilder(); + HostSearch.and("host", HostSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostSearch.done(); + + LastHostSearch = createSearchBuilder(); + LastHostSearch.and("lastHost", LastHostSearch.entity().getLastHostId(), SearchCriteria.Op.EQ); + LastHostSearch.done(); + + ZoneSearch = createSearchBuilder(); + ZoneSearch.and("zone", ZoneSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + ZoneSearch.done(); + + ZoneTemplateNonExpungedSearch = createSearchBuilder(); + ZoneTemplateNonExpungedSearch.and("zone", ZoneTemplateNonExpungedSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + ZoneTemplateNonExpungedSearch.and("template", ZoneTemplateNonExpungedSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + ZoneTemplateNonExpungedSearch.and("state", ZoneTemplateNonExpungedSearch.entity().getState(), SearchCriteria.Op.NEQ); + ZoneTemplateNonExpungedSearch.done(); + + NameLikeSearch = createSearchBuilder(); + NameLikeSearch.and("name", NameLikeSearch.entity().getName(), SearchCriteria.Op.LIKE); + NameLikeSearch.done(); + + StateChangeSearch = createSearchBuilder(); + StateChangeSearch.and("id", StateChangeSearch.entity().getId(), SearchCriteria.Op.EQ); + StateChangeSearch.and("states", StateChangeSearch.entity().getState(), SearchCriteria.Op.EQ); + StateChangeSearch.and("host", StateChangeSearch.entity().getHostId(), SearchCriteria.Op.EQ); + StateChangeSearch.and("update", StateChangeSearch.entity().getUpdated(), SearchCriteria.Op.EQ); + StateChangeSearch.done(); + + TransitionSearch = createSearchBuilder(); + TransitionSearch.and("updateTime", TransitionSearch.entity().getUpdateTime(), SearchCriteria.Op.LT); + TransitionSearch.and("states", TransitionSearch.entity().getState(), SearchCriteria.Op.IN); + TransitionSearch.done(); + + TypesSearch = createSearchBuilder(); + TypesSearch.and("types", TypesSearch.entity().getType(), SearchCriteria.Op.IN); + TypesSearch.done(); + + IdTypesSearch = createSearchBuilder(); + IdTypesSearch.and("id", IdTypesSearch.entity().getId(), SearchCriteria.Op.EQ); + IdTypesSearch.and("types", IdTypesSearch.entity().getType(), SearchCriteria.Op.IN); + IdTypesSearch.done(); + + HostIdTypesSearch = createSearchBuilder(); + HostIdTypesSearch.and("hostid", HostIdTypesSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostIdTypesSearch.and("types", HostIdTypesSearch.entity().getType(), SearchCriteria.Op.IN); + HostIdTypesSearch.done(); + + HostIdUpTypesSearch = createSearchBuilder(); + HostIdUpTypesSearch.and("hostid", HostIdUpTypesSearch.entity().getHostId(), SearchCriteria.Op.EQ); + HostIdUpTypesSearch.and("types", HostIdUpTypesSearch.entity().getType(), SearchCriteria.Op.IN); + HostIdUpTypesSearch.and("states", HostIdUpTypesSearch.entity().getState(), SearchCriteria.Op.NIN); + HostIdUpTypesSearch.done(); + + _updateTimeAttr = _allAttributes.get("updateTime"); + assert _updateTimeAttr != null : "Couldn't get this updateTime attribute"; + } + + @Override + public List findVMInstancesLike(String name) { + SearchCriteria sc = NameLikeSearch.create(); + sc.setParameters("name", "%" + name + "%"); + return listActiveBy(sc); + } + + @Override + public boolean updateIf(VMInstanceVO vm, VirtualMachine.Event event, Long hostId) { + + State oldState = vm.getState(); + State newState = oldState.getNextState(event); + + if (newState == null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("There's no way to transition from old state: " + oldState.toString() + " event: " + event.toString()); + } + return false; + } + + SearchCriteria sc = StateChangeSearch.create(); + sc.setParameters("id", vm.getId()); + sc.setParameters("states", oldState); + sc.setParameters("host", vm.getHostId()); + sc.setParameters("update", vm.getUpdated()); + + vm.incrUpdated(); + UpdateBuilder ub = getUpdateBuilder(vm); + ub.set(vm, "state", newState); + ub.set(vm, "hostId", hostId); + ub.set(vm, _updateTimeAttr, new Date()); + + int result = update(vm, sc); + if (result == 0 && s_logger.isDebugEnabled()) { + VMInstanceVO vo = findById(vm.getId()); + StringBuilder str = new StringBuilder("Unable to update ").append(vo.toString()); + str.append(": DB Data={Host=").append(vo.getHostId()).append("; State=").append(vo.getState().toString()).append("; updated=").append(vo.getUpdated()); + str.append("} Stale Data: {Host=").append(vm.getHostId()).append("; State=").append(vm.getState().toString()).append("; updated=").append(vm.getUpdated()).append("}"); + s_logger.debug(str.toString()); + } + return result > 0; + } + + @Override + public List listByHostId(long hostid) { + SearchCriteria sc = HostSearch.create(); + sc.setParameters("host", hostid); + + return listActiveBy(sc); + } + + @Override + public List listByLastHostId(long hostId) { + SearchCriteria sc = LastHostSearch.create(); + sc.setParameters("lastHost", hostId); + + return listActiveBy(sc); + } + + @Override + public List listByZoneId(long zoneId) { + SearchCriteria sc = ZoneSearch.create(); + sc.setParameters("zone", zoneId); + + return listActiveBy(sc); + } + + @Override + public List listNonExpungedByZoneAndTemplate(long zoneId, long templateId) { + SearchCriteria sc = ZoneTemplateNonExpungedSearch.create(); + + sc.setParameters("zone", zoneId); + sc.setParameters("template", templateId); + sc.setParameters("state", State.Expunging); + + return listActiveBy(sc); + } + + @Override + public List findVMInTransition(Date time, State... states) { + SearchCriteria sc = TransitionSearch.create(); + + sc.setParameters("states", (Object[]) states); + sc.setParameters("updateTime", time); + + return search(sc, null); + } + + public Integer[] countRoutersAndProxies(Long hostId) { + Transaction txn = Transaction.currentTxn(); + PreparedStatement pstmt = null; + Integer[] routerAndProxyCount = new Integer[] { null, null }; + try { + String sql = COUNT_ROUTERS_AND_PROXIES; + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setLong(1, hostId); + pstmt.setLong(2, hostId); + ResultSet rs = pstmt.executeQuery(); + int i = 0; + while (rs.next()) { + routerAndProxyCount[i++] = rs.getInt(1); + } + } catch (Exception e) { + s_logger.warn("Exception searching for routers and proxies", e); + } + return routerAndProxyCount; + } + + + @Override + public List listByHostIdTypes(long hostid, Type... types) { + SearchCriteria sc = HostIdTypesSearch.create(); + sc.setParameters("hostid", hostid); + sc.setParameters("types", (Object[]) types); + return listActiveBy(sc); + } + + @Override + public List listUpByHostIdTypes(long hostid, Type... types) { + SearchCriteria sc = HostIdUpTypesSearch.create(); + sc.setParameters("hostid", hostid); + sc.setParameters("types", (Object[]) types); + sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging}); + return listActiveBy(sc); + } + + @Override + public List listByTypes(Type... types) { + SearchCriteria sc = TypesSearch.create(); + sc.setParameters("types", (Object[]) types); + return listActiveBy(sc); + } + + @Override + public VMInstanceVO findByIdTypes(long id, Type... types) { + SearchCriteria sc = IdTypesSearch.create(); + sc.setParameters("id", id); + sc.setParameters("types", (Object[]) types); + return findOneBy(sc); + } + + + @Override + public void updateProxyId(long id, Long proxyId, Date time) { + VMInstanceVO vo = createForUpdate(); + vo.setProxyId(proxyId); + vo.setProxyAssignTime(time); + update(id, vo); + } +} diff --git a/core/test/com/cloud/async/CleanupDelegate.java b/core/test/com/cloud/async/CleanupDelegate.java new file mode 100644 index 00000000000..7d5d38b47a7 --- /dev/null +++ b/core/test/com/cloud/async/CleanupDelegate.java @@ -0,0 +1,14 @@ +package com.cloud.async; + +import org.apache.log4j.Logger; + +import com.cloud.utils.ActionDelegate; + +public class CleanupDelegate implements ActionDelegate { + private static final Logger s_logger = Logger.getLogger(CleanupDelegate.class); + + @Override + public void action(String param) { + s_logger.info("Action called with param: " + param); + } +} diff --git a/core/test/com/cloud/async/TestAsync.java b/core/test/com/cloud/async/TestAsync.java new file mode 100644 index 00000000000..f653647634d --- /dev/null +++ b/core/test/com/cloud/async/TestAsync.java @@ -0,0 +1,297 @@ +/** + * Copyright (C) 2010 Cloud.com, Inc. All rights reserved. + * + * This software is licensed under the GNU General Public License v3 or later. + * + * It is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.cloud.async; + + +import java.util.List; + +import org.apache.log4j.Logger; + +import junit.framework.Assert; + +import com.cloud.async.AsyncJobResult; +import com.cloud.async.AsyncJobVO; +import com.cloud.async.dao.AsyncJobDao; +import com.cloud.async.dao.AsyncJobDaoImpl; +import com.cloud.maid.StackMaid; +import com.cloud.maid.StackMaidVO; +import com.cloud.maid.dao.StackMaidDao; +import com.cloud.maid.dao.StackMaidDaoImpl; +import com.cloud.serializer.Param; +import com.cloud.serializer.SerializerHelper; +import com.cloud.utils.ActionDelegate; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.testcase.Log4jEnabledTestCase; + + +public class TestAsync extends Log4jEnabledTestCase { + private static final Logger s_logger = Logger.getLogger(TestAsync.class); + + /* + public static class SampleAsyncResult { + @Param(name="name", propName="name") + private final String _name; + + @Param + private final int count; + + public SampleAsyncResult(String name, int count) { + _name = name; + this.count = count; + } + + public String getName() { return _name; } + public int getCount() { return count; } + } + + public void testDao() { + AsyncJobDao dao = new AsyncJobDaoImpl(); + AsyncJobVO job = new AsyncJobVO(1, 1, "TestCmd", null); + job.setInstanceType("user_vm"); + job.setInstanceId(1000L); + + char[] buf = new char[1024]; + for(int i = 0; i < 1024; i++) + buf[i] = 'a'; + + job.setResult(new String(buf)); + dao.persist(job); + + AsyncJobVO jobVerify = dao.findById(job.getId()); + + Assert.assertTrue(jobVerify.getCmd().equals(job.getCmd())); + Assert.assertTrue(jobVerify.getUserId() == 1); + Assert.assertTrue(jobVerify.getAccountId() == 1); + + String result = jobVerify.getResult(); + for(int i = 0; i < 1024; i++) + Assert.assertTrue(result.charAt(i) == 'a'); + + jobVerify = dao.findInstancePendingAsyncJob("user_vm", 1000L); + Assert.assertTrue(jobVerify != null); + Assert.assertTrue(jobVerify.getCmd().equals(job.getCmd())); + Assert.assertTrue(jobVerify.getUserId() == 1); + Assert.assertTrue(jobVerify.getAccountId() == 1); + } + + public void testSerialization() { + List> l; + int value = 1; + l = SerializerHelper.toPairList(value, "result"); + Assert.assertTrue(l.size() == 1); + Assert.assertTrue(l.get(0).first().equals("result")); + Assert.assertTrue(l.get(0).second().equals("1")); + l.clear(); + + SampleAsyncResult result = new SampleAsyncResult("vmops", 1); + l = SerializerHelper.toPairList(result, "result"); + + Assert.assertTrue(l.size() == 2); + Assert.assertTrue(l.get(0).first().equals("name")); + Assert.assertTrue(l.get(0).second().equals("vmops")); + Assert.assertTrue(l.get(1).first().equals("count")); + Assert.assertTrue(l.get(1).second().equals("1")); + } + + public void testAsyncResult() { + AsyncJobResult result = new AsyncJobResult(1); + + result.setResultObject(100); + Assert.assertTrue(result.getResult().equals("java.lang.Integer/100")); + + Object obj = result.getResultObject(); + Assert.assertTrue(obj instanceof Integer); + Assert.assertTrue(((Integer)obj).intValue() == 100); + } + + public void testTransaction() { + Transaction txn = Transaction.open("testTransaction"); + try { + txn.start(); + + AsyncJobDao dao = new AsyncJobDaoImpl(); + AsyncJobVO job = new AsyncJobVO(1, 1, "TestCmd", null); + job.setInstanceType("user_vm"); + job.setInstanceId(1000L); + job.setResult(""); + dao.persist(job); + txn.rollback(); + } finally { + txn.close(); + } + } + + public void testMorevingian() { + int threadCount = 10; + final int testCount = 10; + + Thread[] threads = new Thread[threadCount]; + for(int i = 0; i < threadCount; i++) { + final int threadNum = i + 1; + threads[i] = new Thread(new Runnable() { + public void run() { + for(int i = 0; i < testCount; i++) { + Transaction txn = Transaction.open(Transaction.CLOUD_DB); + try { + AsyncJobDao dao = new AsyncJobDaoImpl(); + + s_logger.info("Thread " + threadNum + " acquiring lock"); + AsyncJobVO job = dao.acquire(1L, 30); + if(job != null) { + s_logger.info("Thread " + threadNum + " acquired lock"); + + try { + Thread.sleep(Log4jEnabledTestCase.getRandomMilliseconds(1000, 3000)); + } catch (InterruptedException e) { + } + + s_logger.info("Thread " + threadNum + " acquiring lock nestly"); + AsyncJobVO job2 = dao.acquire(1L, 30); + if(job2 != null) { + s_logger.info("Thread " + threadNum + " acquired lock nestly"); + + try { + Thread.sleep(Log4jEnabledTestCase.getRandomMilliseconds(1000, 3000)); + } catch (InterruptedException e) { + } + + s_logger.info("Thread " + threadNum + " releasing lock (nestly acquired)"); + dao.release(1L); + s_logger.info("Thread " + threadNum + " released lock (nestly acquired)"); + + } else { + s_logger.info("Thread " + threadNum + " was unable to acquire lock nestly"); + } + + s_logger.info("Thread " + threadNum + " releasing lock"); + dao.release(1L); + s_logger.info("Thread " + threadNum + " released lock"); + } else { + s_logger.info("Thread " + threadNum + " was unable to acquire lock"); + } + } finally { + txn.close(); + } + + try { + Thread.sleep(Log4jEnabledTestCase.getRandomMilliseconds(1000, 10000)); + } catch (InterruptedException e) { + } + } + } + }); + } + + for(int i = 0; i < threadCount; i++) { + threads[i].start(); + } + + for(int i = 0; i < threadCount; i++) { + try { + threads[i].join(); + } catch (InterruptedException e) { + } + } + } + */ + + public void testMaid() { + Transaction txn = Transaction.open(Transaction.CLOUD_DB); + + StackMaidDao dao = new StackMaidDaoImpl(); + dao.pushCleanupDelegate(1L, 0, "delegate1", "Hello, world"); + dao.pushCleanupDelegate(1L, 1, "delegate2", new Long(100)); + dao.pushCleanupDelegate(1L, 2, "delegate3", null); + + StackMaidVO item = dao.popCleanupDelegate(1L); + Assert.assertTrue(item.getDelegate().equals("delegate3")); + Assert.assertTrue(item.getContext() == null); + + item = dao.popCleanupDelegate(1L); + Assert.assertTrue(item.getDelegate().equals("delegate2")); + s_logger.info(item.getContext()); + + item = dao.popCleanupDelegate(1L); + Assert.assertTrue(item.getDelegate().equals("delegate1")); + s_logger.info(item.getContext()); + + txn.close(); + } + + public void testMaidClear() { + Transaction txn = Transaction.open(Transaction.CLOUD_DB); + + StackMaidDao dao = new StackMaidDaoImpl(); + dao.pushCleanupDelegate(1L, 0, "delegate1", "Hello, world"); + dao.pushCleanupDelegate(1L, 1, "delegate2", new Long(100)); + dao.pushCleanupDelegate(1L, 2, "delegate3", null); + + dao.clearStack(1L); + Assert.assertTrue(dao.popCleanupDelegate(1L) == null); + txn.close(); + } + + public void testMaidExitCleanup() { + StackMaid.current().push(1L, "com.cloud.async.CleanupDelegate", "Hello, world1"); + StackMaid.current().push(1L, "com.cloud.async.CleanupDelegate", "Hello, world2"); + + StackMaid.current().exitCleanup(1L); + } + + public void testMaidLeftovers() { + + Thread[] threads = new Thread[3]; + for(int i = 0; i < 3; i++) { + final int threadNum = i+1; + threads[i] = new Thread(new Runnable() { + public void run() { + Transaction txn = Transaction.open(Transaction.CLOUD_DB); + + StackMaidDao dao = new StackMaidDaoImpl(); + dao.pushCleanupDelegate(1L, 0, "delegate-" + threadNum, "Hello, world"); + dao.pushCleanupDelegate(1L, 1, "delegate-" + threadNum, new Long(100)); + dao.pushCleanupDelegate(1L, 2, "delegate-" + threadNum, null); + + txn.close(); + } + }); + + threads[i].start(); + } + + for(int i = 0; i < 3; i++) { + try { + threads[i].join(); + } catch (InterruptedException e) { + } + } + + + Transaction txn = Transaction.open(Transaction.CLOUD_DB); + + StackMaidDao dao = new StackMaidDaoImpl(); + List l = dao.listLeftoversByMsid(1L); + for(StackMaidVO maid : l) { + s_logger.info("" + maid.getThreadId() + " " + maid.getDelegate() + " " + maid.getContext()); + } + + txn.close(); + } +} diff --git a/core/test/com/cloud/vmware/TestVMWare.java b/core/test/com/cloud/vmware/TestVMWare.java new file mode 100644 index 00000000000..1c7f4aa633b --- /dev/null +++ b/core/test/com/cloud/vmware/TestVMWare.java @@ -0,0 +1,651 @@ +package com.cloud.vmware; + +import java.io.File; +import java.rmi.RemoteException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.log4j.xml.DOMConfigurator; + +import com.cloud.utils.PropertiesUtil; +import com.vmware.apputils.AppUtil; +import com.vmware.vim.ArrayOfManagedObjectReference; +import com.vmware.vim.DatastoreInfo; +import com.vmware.vim.DynamicProperty; +import com.vmware.vim.InvalidProperty; +import com.vmware.vim.ManagedObjectReference; +import com.vmware.vim.ObjectContent; +import com.vmware.vim.ObjectSpec; +import com.vmware.vim.PropertyFilterSpec; +import com.vmware.vim.PropertySpec; +import com.vmware.vim.RuntimeFault; +import com.vmware.vim.SelectionSpec; +import com.vmware.vim.TraversalSpec; + +public class TestVMWare { + private static AppUtil cb; + + private static void setupLog4j() { + File file = PropertiesUtil.findConfigFile("log4j-cloud.xml"); + + if(file != null) { + System.out.println("Log4j configuration from : " + file.getAbsolutePath()); + DOMConfigurator.configureAndWatch(file.getAbsolutePath(), 10000); + } else { + System.out.println("Configure log4j with default properties"); + } + } + + private void getAndPrintInventoryContents() throws Exception { + TraversalSpec resourcePoolTraversalSpec = new TraversalSpec(); + resourcePoolTraversalSpec.setName("resourcePoolTraversalSpec"); + resourcePoolTraversalSpec.setType("ResourcePool"); + resourcePoolTraversalSpec.setPath("resourcePool"); + resourcePoolTraversalSpec.setSkip(new Boolean(false)); + resourcePoolTraversalSpec.setSelectSet( + new SelectionSpec [] { new SelectionSpec(null,null,"resourcePoolTraversalSpec") }); + + TraversalSpec computeResourceRpTraversalSpec = new TraversalSpec(); + computeResourceRpTraversalSpec.setName("computeResourceRpTraversalSpec"); + computeResourceRpTraversalSpec.setType("ComputeResource"); + computeResourceRpTraversalSpec.setPath("resourcePool"); + computeResourceRpTraversalSpec.setSkip(new Boolean(false)); + computeResourceRpTraversalSpec.setSelectSet( + new SelectionSpec [] { new SelectionSpec(null,null,"resourcePoolTraversalSpec") }); + + TraversalSpec computeResourceHostTraversalSpec = new TraversalSpec(); + computeResourceHostTraversalSpec.setName("computeResourceHostTraversalSpec"); + computeResourceHostTraversalSpec.setType("ComputeResource"); + computeResourceHostTraversalSpec.setPath("host"); + computeResourceHostTraversalSpec.setSkip(new Boolean(false)); + + TraversalSpec datacenterHostTraversalSpec = new TraversalSpec(); + datacenterHostTraversalSpec.setName("datacenterHostTraversalSpec"); + datacenterHostTraversalSpec.setType("Datacenter"); + datacenterHostTraversalSpec.setPath("hostFolder"); + datacenterHostTraversalSpec.setSkip(new Boolean(false)); + datacenterHostTraversalSpec.setSelectSet( + new SelectionSpec [] { new SelectionSpec(null,null,"folderTraversalSpec") }); + + TraversalSpec datacenterVmTraversalSpec = new TraversalSpec(); + datacenterVmTraversalSpec.setName("datacenterVmTraversalSpec"); + datacenterVmTraversalSpec.setType("Datacenter"); + datacenterVmTraversalSpec.setPath("vmFolder"); + datacenterVmTraversalSpec.setSkip(new Boolean(false)); + datacenterVmTraversalSpec.setSelectSet( + new SelectionSpec [] { new SelectionSpec(null,null,"folderTraversalSpec") }); + + TraversalSpec folderTraversalSpec = new TraversalSpec(); + folderTraversalSpec.setName("folderTraversalSpec"); + folderTraversalSpec.setType("Folder"); + folderTraversalSpec.setPath("childEntity"); + folderTraversalSpec.setSkip(new Boolean(false)); + folderTraversalSpec.setSelectSet( + new SelectionSpec [] { new SelectionSpec(null,null,"folderTraversalSpec"), + datacenterHostTraversalSpec, + datacenterVmTraversalSpec, + computeResourceRpTraversalSpec, + computeResourceHostTraversalSpec, + resourcePoolTraversalSpec }); + + PropertySpec[] propspecary = new PropertySpec[] { new PropertySpec() }; + propspecary[0].setAll(new Boolean(false)); + propspecary[0].setPathSet(new String[] { "name" }); + propspecary[0].setType("ManagedEntity"); + + PropertyFilterSpec spec = new PropertyFilterSpec(); + spec.setPropSet(propspecary); + spec.setObjectSet(new ObjectSpec[] { new ObjectSpec() }); + spec.getObjectSet(0).setObj(cb.getConnection().getRootFolder()); + spec.getObjectSet(0).setSkip(new Boolean(false)); + spec.getObjectSet(0).setSelectSet( + new SelectionSpec[] { folderTraversalSpec }); + + // Recursively get all ManagedEntity ManagedObjectReferences + // and the "name" property for all ManagedEntities retrieved + ObjectContent[] ocary = + cb.getConnection().getService().retrieveProperties( + cb.getConnection().getServiceContent().getPropertyCollector(), + new PropertyFilterSpec[] { spec } + ); + + // If we get contents back. print them out. + if (ocary != null) { + ObjectContent oc = null; + ManagedObjectReference mor = null; + DynamicProperty[] pcary = null; + DynamicProperty pc = null; + for (int oci = 0; oci < ocary.length; oci++) { + oc = ocary[oci]; + mor = oc.getObj(); + pcary = oc.getPropSet(); + + System.out.println("Object Type : " + mor.getType()); + System.out.println("Reference Value : " + mor.get_value()); + + if (pcary != null) { + for (int pci = 0; pci < pcary.length; pci++) { + pc = pcary[pci]; + System.out.println(" Property Name : " + pc.getName()); + if (pc != null) { + if (!pc.getVal().getClass().isArray()) { + System.out.println(" Property Value : " + pc.getVal()); + } + else { + Object[] ipcary = (Object[])pc.getVal(); + System.out.println("Val : " + pc.getVal()); + for (int ii = 0; ii < ipcary.length; ii++) { + Object oval = ipcary[ii]; + if (oval.getClass().getName().indexOf("ManagedObjectReference") >= 0) { + ManagedObjectReference imor = (ManagedObjectReference)oval; + + System.out.println("Inner Object Type : " + imor.getType()); + System.out.println("Inner Reference Value : " + imor.get_value()); + } + else { + System.out.println("Inner Property Value : " + oval); + } + } + } + } + } + } + } + } else { + System.out.println("No Managed Entities retrieved!"); + } + } + + private void listDataCenters() { + try { + ManagedObjectReference[] morDatacenters = getDataCenterMors(); + if(morDatacenters != null) { + for(ManagedObjectReference mor : morDatacenters) { + System.out.println("Datacenter : " + mor.get_value()); + + Map properites = new HashMap(); + properites.put("name", null); + properites.put("vmFolder", null); + properites.put("hostFolder", null); + + getProperites(mor, properites); + for(Map.Entry entry : properites.entrySet()) { + if(entry.getValue() instanceof ManagedObjectReference) { + ManagedObjectReference morProp = (ManagedObjectReference)entry.getValue(); + System.out.println("\t" + entry.getKey() + ":(" + morProp.getType() + ", " + morProp.get_value() + ")"); + } else { + System.out.println("\t" + entry.getKey() + ":" + entry.getValue()); + } + } + + System.out.println("Datacenter clusters"); + ManagedObjectReference[] clusters = getDataCenterClusterMors(mor); + if(clusters != null) { + for(ManagedObjectReference morCluster : clusters) { + Object[] props = this.getProperties(morCluster, new String[] {"name"}); + System.out.println("cluster : " + props[0]); + + System.out.println("cluster hosts"); + ManagedObjectReference[] hosts = getClusterHostMors(morCluster); + if(hosts != null) { + for(ManagedObjectReference morHost : hosts) { + Object[] props2 = this.getProperties(morHost, new String[] {"name"}); + System.out.println("host : " + props2[0]); + } + } + } + } + + System.out.println("Datacenter standalone hosts"); + ManagedObjectReference[] hosts = getDataCenterStandaloneHostMors(mor); + if(hosts != null) { + for(ManagedObjectReference morHost : hosts) { + Object[] props = this.getProperties(morHost, new String[] {"name"}); + System.out.println("host : " + props[0]); + } + } + + System.out.println("Datacenter datastores"); + ManagedObjectReference[] stores = getDataCenterDatastoreMors(mor); + if(stores != null) { + for(ManagedObjectReference morStore : stores) { + // data store name property does not work for some reason + Object[] props = getProperties(morStore, new String[] {"info" }); + + System.out.println(morStore.getType() + ": " + ((DatastoreInfo)props[0]).getName()); + } + } + + System.out.println("Datacenter VMs"); + ManagedObjectReference[] vms = getDataCenterVMMors(mor); + if(stores != null) { + for(ManagedObjectReference morVm : vms) { + Object[] props = this.getProperties(morVm, new String[] {"name"}); + System.out.println("VM name: " + props[0] + ", ref val: " + morVm.get_value()); + } + } + } + } + } catch(RuntimeFault e) { + e.printStackTrace(); + } catch(RemoteException e) { + e.printStackTrace(); + } + } + + private void listInventoryFolders() { + TraversalSpec folderTraversalSpec = new TraversalSpec(); + folderTraversalSpec.setName("folderTraversalSpec"); + folderTraversalSpec.setType("Folder"); + folderTraversalSpec.setPath("childEntity"); + folderTraversalSpec.setSkip(new Boolean(false)); + folderTraversalSpec.setSelectSet( + new SelectionSpec [] { new SelectionSpec(null, null, "folderTraversalSpec")} + ); + + PropertySpec[] propSpecs = new PropertySpec[] { new PropertySpec() }; + propSpecs[0].setAll(new Boolean(false)); + propSpecs[0].setPathSet(new String[] { "name" }); + propSpecs[0].setType("ManagedEntity"); + + PropertyFilterSpec filterSpec = new PropertyFilterSpec(); + filterSpec.setPropSet(propSpecs); + filterSpec.setObjectSet(new ObjectSpec[] { new ObjectSpec() }); + filterSpec.getObjectSet(0).setObj(cb.getConnection().getRootFolder()); + filterSpec.getObjectSet(0).setSkip(new Boolean(false)); + filterSpec.getObjectSet(0).setSelectSet( + new SelectionSpec[] { folderTraversalSpec } + ); + + try { + ObjectContent[] objContent = cb.getConnection().getService().retrieveProperties( + cb.getConnection().getServiceContent().getPropertyCollector(), + new PropertyFilterSpec[] { filterSpec } + ); + printContent(objContent); + } catch (InvalidProperty e) { + e.printStackTrace(); + } catch (RuntimeFault e) { + e.printStackTrace(); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + private TraversalSpec getFolderRecursiveTraversalSpec() { + SelectionSpec recurseFolders = new SelectionSpec(); + recurseFolders.setName("folder2childEntity"); + + TraversalSpec folder2childEntity = new TraversalSpec(); + folder2childEntity.setType("Folder"); + folder2childEntity.setPath("childEntity"); + folder2childEntity.setName(recurseFolders.getName()); + folder2childEntity.setSelectSet(new SelectionSpec[] { recurseFolders }); + + return folder2childEntity; + } + + private ManagedObjectReference[] getDataCenterMors() throws RuntimeFault, RemoteException { + PropertySpec pSpec = new PropertySpec(); + pSpec.setType("Datacenter"); + pSpec.setPathSet(new String[] { "name"} ); + + ObjectSpec oSpec = new ObjectSpec(); + oSpec.setObj(cb.getConnection().getRootFolder()); + oSpec.setSkip(Boolean.TRUE); + oSpec.setSelectSet(new SelectionSpec[] { getFolderRecursiveTraversalSpec() }); + + PropertyFilterSpec pfSpec = new PropertyFilterSpec(); + pfSpec.setPropSet(new PropertySpec[] { pSpec }); + pfSpec.setObjectSet(new ObjectSpec[] { oSpec }); + + ObjectContent[] ocs = cb.getConnection().getService().retrieveProperties( + cb.getConnection().getServiceContent().getPropertyCollector(), + new PropertyFilterSpec[] { pfSpec }); + + if(ocs != null) { + ManagedObjectReference[] morDatacenters = new ManagedObjectReference[ocs.length]; + for(int i = 0; i < ocs.length; i++) + morDatacenters[i] = ocs[i].getObj(); + + return morDatacenters; + } + return null; + } + + private ManagedObjectReference[] getDataCenterVMMors(ManagedObjectReference morDatacenter) throws RuntimeFault, RemoteException { + PropertySpec pSpec = new PropertySpec(); + pSpec.setType("VirtualMachine"); + pSpec.setPathSet(new String[] { "name"} ); + + ObjectSpec oSpec = new ObjectSpec(); + oSpec.setObj(morDatacenter); + oSpec.setSkip(Boolean.TRUE); + + TraversalSpec tSpec = new TraversalSpec(); + tSpec.setName("dc2VMFolder"); + tSpec.setType("Datacenter"); + tSpec.setPath("vmFolder"); + tSpec.setSelectSet(new SelectionSpec[] { getFolderRecursiveTraversalSpec() } ); + + oSpec.setSelectSet(new SelectionSpec[] { tSpec }); + + PropertyFilterSpec pfSpec = new PropertyFilterSpec(); + pfSpec.setPropSet(new PropertySpec[] { pSpec }); + pfSpec.setObjectSet(new ObjectSpec[] { oSpec }); + + ObjectContent[] ocs = cb.getConnection().getService().retrieveProperties( + cb.getConnection().getServiceContent().getPropertyCollector(), + new PropertyFilterSpec[] { pfSpec }); + + if(ocs != null) { + ManagedObjectReference[] morVMs = new ManagedObjectReference[ocs.length]; + for(int i = 0; i < ocs.length; i++) + morVMs[i] = ocs[i].getObj(); + + return morVMs; + } + return null; + } + + private ManagedObjectReference[] getDataCenterDatastoreMors(ManagedObjectReference morDatacenter) throws RuntimeFault, RemoteException { + Object[] stores = getProperties(morDatacenter, new String[] { "datastore" }); + if(stores != null && stores.length == 1) { + return ((ArrayOfManagedObjectReference)stores[0]).getManagedObjectReference(); + } + return null; + } + + private ManagedObjectReference[] getDataCenterClusterMors(ManagedObjectReference morDatacenter) throws RuntimeFault, RemoteException { + PropertySpec pSpec = new PropertySpec(); + pSpec.setType("ClusterComputeResource"); + pSpec.setPathSet(new String[] { "name"} ); + + ObjectSpec oSpec = new ObjectSpec(); + oSpec.setObj(morDatacenter); + oSpec.setSkip(Boolean.TRUE); + + TraversalSpec tSpec = new TraversalSpec(); + tSpec.setName("traversalHostFolder"); + tSpec.setType("Datacenter"); + tSpec.setPath("hostFolder"); + tSpec.setSkip(false); + tSpec.setSelectSet(new SelectionSpec[] { getFolderRecursiveTraversalSpec() }); + + oSpec.setSelectSet(new TraversalSpec[] { tSpec }); + + PropertyFilterSpec pfSpec = new PropertyFilterSpec(); + pfSpec.setPropSet(new PropertySpec[] { pSpec }); + pfSpec.setObjectSet(new ObjectSpec[] { oSpec }); + + ObjectContent[] ocs = cb.getConnection().getService().retrieveProperties( + cb.getConnection().getServiceContent().getPropertyCollector(), + new PropertyFilterSpec[] { pfSpec }); + + if(ocs != null) { + ManagedObjectReference[] morDatacenters = new ManagedObjectReference[ocs.length]; + for(int i = 0; i < ocs.length; i++) + morDatacenters[i] = ocs[i].getObj(); + + return morDatacenters; + } + return null; + } + + private ManagedObjectReference[] getDataCenterStandaloneHostMors(ManagedObjectReference morDatacenter) throws RuntimeFault, RemoteException { + PropertySpec pSpec = new PropertySpec(); + pSpec.setType("ComputeResource"); + pSpec.setPathSet(new String[] { "name"} ); + + ObjectSpec oSpec = new ObjectSpec(); + oSpec.setObj(morDatacenter); + oSpec.setSkip(Boolean.TRUE); + + TraversalSpec tSpec = new TraversalSpec(); + tSpec.setName("traversalHostFolder"); + tSpec.setType("Datacenter"); + tSpec.setPath("hostFolder"); + tSpec.setSkip(false); + tSpec.setSelectSet(new SelectionSpec[] { getFolderRecursiveTraversalSpec() }); + + oSpec.setSelectSet(new TraversalSpec[] { tSpec }); + + PropertyFilterSpec pfSpec = new PropertyFilterSpec(); + pfSpec.setPropSet(new PropertySpec[] { pSpec }); + pfSpec.setObjectSet(new ObjectSpec[] { oSpec }); + + ObjectContent[] ocs = cb.getConnection().getService().retrieveProperties( + cb.getConnection().getServiceContent().getPropertyCollector(), + new PropertyFilterSpec[] { pfSpec }); + + if(ocs != null) { + List listComputeResources = new ArrayList(); + for(ObjectContent oc : ocs) { + if(oc.getObj().getType().equalsIgnoreCase("ComputeResource")) + listComputeResources.add(oc.getObj()); + } + + List listHosts = new ArrayList(); + for(ManagedObjectReference morComputeResource : listComputeResources) { + ManagedObjectReference[] hosts = getComputeResourceHostMors(morComputeResource); + if(hosts != null) { + for(ManagedObjectReference host: hosts) + listHosts.add(host); + } + } + + return listHosts.toArray(new ManagedObjectReference[0]); + } + return null; + } + + private ManagedObjectReference[] getComputeResourceHostMors(ManagedObjectReference morCompute) throws RuntimeFault, RemoteException { + PropertySpec pSpec = new PropertySpec(); + pSpec.setType("HostSystem"); + pSpec.setPathSet(new String[] { "name"} ); + + ObjectSpec oSpec = new ObjectSpec(); + oSpec.setObj(morCompute); + oSpec.setSkip(true); + + TraversalSpec tSpec = new TraversalSpec(); + tSpec.setName("computeResource2Host"); + tSpec.setType("ComputeResource"); + tSpec.setPath("host"); + tSpec.setSkip(false); + oSpec.setSelectSet(new TraversalSpec[] { tSpec }); + + PropertyFilterSpec pfSpec = new PropertyFilterSpec(); + pfSpec.setPropSet(new PropertySpec[] { pSpec }); + pfSpec.setObjectSet(new ObjectSpec[] { oSpec }); + + ObjectContent[] ocs = cb.getConnection().getService().retrieveProperties( + cb.getConnection().getServiceContent().getPropertyCollector(), + new PropertyFilterSpec[] { pfSpec }); + + if(ocs != null) { + ManagedObjectReference[] morDatacenters = new ManagedObjectReference[ocs.length]; + for(int i = 0; i < ocs.length; i++) + morDatacenters[i] = ocs[i].getObj(); + + return morDatacenters; + } + return null; + } + + private ManagedObjectReference[] getClusterHostMors(ManagedObjectReference morCluster) throws RuntimeFault, RemoteException { + // ClusterComputeResource inherits from ComputeResource + return getComputeResourceHostMors(morCluster); + } + + private ObjectContent[] getDataCenterProperites(String[] properites) throws RuntimeFault, RemoteException { + PropertySpec pSpec = new PropertySpec(); + pSpec.setType("Datacenter"); + pSpec.setPathSet(properites ); + + SelectionSpec recurseFolders = new SelectionSpec(); + recurseFolders.setName("folder2childEntity"); + + TraversalSpec folder2childEntity = new TraversalSpec(); + folder2childEntity.setType("Folder"); + folder2childEntity.setPath("childEntity"); + folder2childEntity.setName(recurseFolders.getName()); + folder2childEntity.setSelectSet(new SelectionSpec[] { recurseFolders }); + + ObjectSpec oSpec = new ObjectSpec(); + oSpec.setObj(cb.getConnection().getRootFolder()); + oSpec.setSkip(Boolean.TRUE); + oSpec.setSelectSet(new SelectionSpec[] { folder2childEntity }); + + PropertyFilterSpec pfSpec = new PropertyFilterSpec(); + pfSpec.setPropSet(new PropertySpec[] { pSpec }); + pfSpec.setObjectSet(new ObjectSpec[] { oSpec }); + + return cb.getConnection().getService().retrieveProperties( + cb.getConnection().getServiceContent().getPropertyCollector(), + new PropertyFilterSpec[] { pfSpec }); + } + + private void printContent(ObjectContent[] objContent) { + if(objContent != null) { + for(ObjectContent oc : objContent) { + ManagedObjectReference mor = oc.getObj(); + DynamicProperty[] objProps = oc.getPropSet(); + + System.out.println("Object type: " + mor.getType()); + if(objProps != null) { + for(DynamicProperty objProp : objProps) { + if(!objProp.getClass().isArray()) { + System.out.println("\t" + objProp.getName() + "=" + objProp.getVal()); + } else { + Object[] ipcary = (Object[])objProp.getVal(); + System.out.print("\t" + objProp.getName() + "=["); + int i = 0; + for(Object item : ipcary) { + if (item.getClass().getName().indexOf("ManagedObjectReference") >= 0) { + ManagedObjectReference imor = (ManagedObjectReference)item; + System.out.print("(" + imor.getType() + "," + imor.get_value() + ")"); + } else { + System.out.print(item); + } + + if(i < ipcary.length - 1) + System.out.print(", "); + i++; + } + + System.out.println("]"); + } + } + } + } + } + } + + private void getProperites(ManagedObjectReference mor, Map properties) throws RuntimeFault, RemoteException { + PropertySpec pSpec = new PropertySpec(); + pSpec.setType(mor.getType()); + pSpec.setPathSet(properties.keySet().toArray(new String[0])); + + ObjectSpec oSpec = new ObjectSpec(); + oSpec.setObj(mor); + + PropertyFilterSpec pfSpec = new PropertyFilterSpec(); + pfSpec.setPropSet(new PropertySpec[] {pSpec} ); + pfSpec.setObjectSet(new ObjectSpec[] {oSpec} ); + + ObjectContent[] ocs = cb.getConnection().getService().retrieveProperties( + cb.getConnection().getServiceContent().getPropertyCollector(), + new PropertyFilterSpec[] {pfSpec} ); + + if(ocs != null) { + for(ObjectContent oc : ocs) { + DynamicProperty[] propSet = oc.getPropSet(); + if(propSet != null) { + for(DynamicProperty prop : propSet) { + properties.put(prop.getName(), prop.getVal()); + } + } + } + } + } + + private Object[] getProperties(ManagedObjectReference moRef, String[] properties) throws RuntimeFault, RemoteException { + PropertySpec pSpec = new PropertySpec(); + pSpec.setType(moRef.getType()); + pSpec.setPathSet(properties); + + ObjectSpec oSpec = new ObjectSpec(); + // Set the starting object + oSpec.setObj(moRef); + + PropertyFilterSpec pfSpec = new PropertyFilterSpec(); + pfSpec.setPropSet(new PropertySpec[] {pSpec} ); + pfSpec.setObjectSet(new ObjectSpec[] {oSpec} ); + ObjectContent[] ocs = cb.getConnection().getService().retrieveProperties( + cb.getConnection().getServiceContent().getPropertyCollector(), + new PropertyFilterSpec[] {pfSpec} ); + + Object[] ret = new Object[properties.length]; + if(ocs != null) { + for(int i = 0; i< ocs.length; ++i) { + ObjectContent oc = ocs[i]; + DynamicProperty[] dps = oc.getPropSet(); + if(dps != null) { + for(int j = 0; j < dps.length; ++j) { + DynamicProperty dp = dps[j]; + for(int p = 0; p < ret.length; ++p) { + if(properties[p].equals(dp.getName())) { + ret[p] = dp.getVal(); + } + } + } + } + } + } + return ret; + } + + private void powerOnVm() throws Exception { + ManagedObjectReference morVm = new ManagedObjectReference(); + morVm.setType("VirtualMachine"); + morVm.set_value("vm-66"); + + cb.getConnection().getService().powerOnVM_Task(morVm, null); + } + + private void powerOffVm() throws Exception { + ManagedObjectReference morVm = new ManagedObjectReference(); + morVm.setType("VirtualMachine"); + morVm.set_value("vm-66"); + + cb.getConnection().getService().powerOffVM_Task(morVm); + } + + public static void main(String[] args) throws Exception { + setupLog4j(); + TestVMWare client = new TestVMWare(); + + // skip certificate check + System.setProperty("axis.socketSecureFactory", "org.apache.axis.components.net.SunFakeTrustSocketFactory"); + + String serviceUrl = "https://vsphere-1.lab.vmops.com/sdk/vimService"; + try { + String[] params = new String[] {"--url", serviceUrl, "--username", "Administrator", "--password", "Suite219" }; + + cb = AppUtil.initialize("Connect", params); + cb.connect(); + System.out.println("Connection Succesful."); + + // client.listInventoryFolders(); + // client.listDataCenters(); + client.powerOnVm(); + + cb.disConnect(); + } catch (Exception e) { + System.out.println("Failed to connect to " + serviceUrl); + } + } +} diff --git a/daemonize/COPYING b/daemonize/COPYING new file mode 100644 index 00000000000..d60c31a97a5 --- /dev/null +++ b/daemonize/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/daemonize/daemonize.c b/daemonize/daemonize.c new file mode 100644 index 00000000000..0788ac345e9 --- /dev/null +++ b/daemonize/daemonize.c @@ -0,0 +1,332 @@ +/* +UNIX daemonizer. Daemonizes any non-interactive console program and watches over it. +Whenever a signal is sent to this process, it halts the daemonized process as well. + +To compile: cc -o daemonize daemonize.c +Usage: ./daemonize -? +Users of this: catalina initscript +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define RUNNING_DIR "/" +#define PIDFILE "/var/run/daemonize.pid" +#define VARLOGFILE "/var/log/daemon.log" +#define PROGNAME "daemonized" +#define DEFAULTUSER "root" + +char * pidfile = PIDFILE; +char * varlogfile = VARLOGFILE; +char * progname = PROGNAME; +char * user = PROGNAME; + +void initialize_syslog(const char*pn) { + openlog(pn,LOG_PID,LOG_DAEMON); + syslog(LOG_INFO, "syslog connection opened"); +} + +void cleanup_syslog() { + syslog(LOG_INFO, "syslog connection closed"); + closelog(); +} + +int killed = 0; +int killsignal = 0; +int pidfile_fd; +int varlogfile_fd; +int uid = 0; int gid = 0; +struct passwd *creds; + +void signal_handler(sig) +int sig; +{ + killsignal = sig; + switch(sig) { + case SIGCHLD: + syslog(LOG_INFO,"sigchild signal caught"); + break; + case SIGHUP: + syslog(LOG_INFO,"hangup signal caught"); + killed = 1; + break; + case SIGTERM: + syslog(LOG_INFO,"terminate signal caught"); + killed = 1; + break; + case SIGINT: + syslog(LOG_INFO,"keyboard interrupt signal caught"); + killed = 1; + break; + } +} + +int daemonize(const char*prog_name) +{ + + char str[10]; + int i; + int bufsize=1024; char *buf = malloc(1024); + + umask( S_IWGRP | S_IROTH | S_IWOTH ); /* set newly created file permissions */ + + /* test logfile */ + varlogfile_fd=open(varlogfile,O_RDWR|O_CREAT|O_APPEND,0666); + if (varlogfile_fd == -1) { + snprintf(buf,bufsize,"Could not open output file %s -- exiting",varlogfile); perror(buf); + return 1; /* exitvalue */ + } + if (uid != 0) { + chown(varlogfile,uid,gid); + } + close(varlogfile_fd); + pidfile_fd=open(pidfile,O_RDWR|O_CREAT,0666); + if (pidfile_fd<0) { + snprintf(buf,bufsize,"The PID file %s cannot be opened -- exiting",pidfile); perror(buf); + return 2; /* exitvalue */ + } + if (lockf(pidfile_fd,F_TEST,0)==1) { + snprintf(buf,bufsize,"A daemon is already running (cannot lock PID file %s) -- exiting",pidfile); perror(buf); + return 3; /* exitvalue */ + } + close(pidfile_fd); + + if(getppid()==1) return 0; /* already a daemon */ + i=fork(); + if (i < 0) return 4; /* exitvalue */ /* fork error */ + if (i > 0) exit(0); /* parent exits */ + + /* child (daemon) continues */ + setsid(); /* obtain a new process group */ + + chdir(RUNNING_DIR); /* change running directory */ + + /* close FDs and reopen to logfile */ + for (i=getdtablesize();i>=0;--i) close(i); /* close all descriptors */ + varlogfile_fd=open(varlogfile,O_RDWR|O_APPEND,0666); dup(varlogfile_fd); dup(varlogfile_fd); /* handle standart I/O */ + initialize_syslog(prog_name); /* set up syslog */ + + /* PID file */ + pidfile_fd=open(pidfile,O_RDWR|O_CREAT,0666); + if (pidfile_fd<0) { + syslog(LOG_ERR,"The PID file %s cannot be opened (%m) -- exiting",pidfile); + return 2; /* exitvalue */ + } + if (lockf(pidfile_fd,F_TLOCK,0)<0) { + syslog(LOG_ERR,"A daemon is already running -- cannot lock PID file %s (%m) -- exiting",pidfile); + return 3; /* exitvalue */ + } + + /* first instance continues */ + + /* record pid to pidfile */ + sprintf(str,"%d\n",getpid()); + if (write(pidfile_fd,str,strlen(str)) < strlen(str)) { + syslog(LOG_ERR,"Could not write PID into PID file %s (%m) -- exiting",pidfile); + return 5; /* exitvalue */ + } + signal(SIGTSTP,SIG_IGN); /* ignore tty signals */ + signal(SIGTTOU,SIG_IGN); + signal(SIGTTIN,SIG_IGN); + signal(SIGHUP,signal_handler); /* catch hangup signal */ + signal(SIGTERM,signal_handler); /* catch kill signal */ + signal(SIGINT,signal_handler); /* catch keyboard interrupt signal */ + + return 0; +} + +void cleanup() { + cleanup_syslog(); + unlink(pidfile); + close(pidfile_fd); + close(varlogfile_fd); +} + +void usage(char * cmdname) { + fprintf (stderr, + "Usage: %s [options...] -- [command-specific arguments...]\n" + "Daemonize any program.\n" + "\n" + "Options:\n" + "\n" + " -l : log stdout/stderr to this *absolute* path (default "VARLOGFILE")\n" + " -u : setuid() to this user name before starting the program (default "DEFAULTUSER")\n" + " -p : lock and write the PID to this *absolute* path (default "PIDFILE")\n" + " -n : name the daemon assumes (default "PROGNAME")\n" + " -h: show this usage guide\n" + "\n" + "Exit status:\n" + " 0 if daemonized correctly\n" + " other if an error took place\n" + "", cmdname); + exit(0); +} + +int parse_args(int argc,char ** argv) { + int index; + int c; + +// pidfile = PIDFILE; +// varlogfile = VARLOGFILE; +// progname = PROGNAME; + + opterr = 0; + + while ((c = getopt (argc, argv, "l:p:n:u:")) != -1) + switch (c) + { + case 'l': + varlogfile = optarg; + break; + case 'p': + pidfile = optarg; + break; + case 'n': + progname = optarg; + break; + case 'u': + if (getuid() != 0) { + fprintf (stderr, "-u can only be used by root.\nSee help with -h\n", user); + exit(64); + } + user = optarg; + creds = getpwnam(user); + if (creds == NULL) { + fprintf (stderr, "User %s was not found in the user database.\nSee help with -h\n", user); + exit(63); + } + uid = creds->pw_uid; gid = creds->pw_gid; + break; +// case 'h': +// break; +// usage(argv[0]); /* halts after this */ + case '?': + if (optopt == '?' || optopt == 'h') + usage(argv[0]); /* halts after this */ + if (optopt == 'l' || optopt == 'p' || optopt == 'n') + fprintf (stderr, "Option -%c requires an argument.\nSee help with -h\n", optopt); + else if (isprint (optopt)) + fprintf (stderr, "Unknown option `-%c'.\nSee help with -h\n", optopt); + else + fprintf (stderr, "Unknown option character `\\x%x'.\nSee help with -h\n", optopt); + exit(64); /* exitvalue */ + default: + abort (); + } + + for (index = optind; index < argc; index++); + + if (index == optind) { + fprintf (stderr, "You need to specify a command to run.\nSee help with -h\n", optopt); + exit(64); /* exitvalue */ + } + + return optind; +} + +int main(int argc, char** argv) +{ + /* parse command line arguments, we will use the first non-option one as the starting point */ + int i; + char ** newargv = calloc(argc+1, sizeof(char**)); + int startat = parse_args(argc,argv); + int newargc = argc - startat; + for (i = startat; i < argc; i++) { newargv[i-startat] = argv[i]; } + + /* try and daemonize */ + int daemonret = daemonize(progname); + if (daemonret) exit(daemonret); + syslog(LOG_INFO,"successfully daemonized"); + + /* fork */ + int pid, wpid, status, execret; + syslog(LOG_INFO,"starting %s in subprocess",newargv[0]); + pid = fork(); + if (pid < 0) { + /* failed to fork, damnit! */ + syslog(LOG_ERR,"could not fork to run %s as a child process (%m)",newargv[0]); + exit(4); /* exitvalue */ + } + else if (pid == 0) { + /* child */ + if (uid != 0) { + execret = setgid(gid); + if (execret == -1) { + syslog(LOG_ERR,"could not setgid() to gid %d",gid); + exit(8); /* exitvalue */ + } + execret = setuid(uid); + if (execret == -1) { + syslog(LOG_ERR,"could not setuid() to uid %d",uid); + exit(8); /* exitvalue */ + } + } + execret = execvp(newargv[0],newargv); + if (errno == 2) { + syslog(LOG_ERR,"could not run program: no such file or directory"); + exit(127); + } + if (errno == 13) { + syslog(LOG_ERR,"could not run program: permission denied"); + exit(126); + } + syslog(LOG_ERR,"could not run program: unknown reason"); + exit(255); + } + + /* parent continues here */ + syslog(LOG_INFO,"successfully started subprocess -- PID %d",pid); + int finalexit = 0; + int waitret = 0; + while (1) { + if (killed) { + kill(pid,killsignal); + killed = 0; + } + waitret = waitpid(pid,&status,WNOHANG); + if (waitret == pid) break; + usleep(250000); + } + + + if WIFEXITED(status) { + switch (WEXITSTATUS(status)) { + case 0: + syslog(LOG_INFO,"%s exited normally",newargv[0]); + break; + case 126: + syslog(LOG_ERR,"%s: permission denied",newargv[0]); + finalexit = 126; /* exitvalue */ + break; + case 127: + syslog(LOG_ERR,"%s: command not found",newargv[0]); + finalexit = 127; /* exitvalue */ + break; + default: + syslog(LOG_INFO,"%s exited abnormally with status %d",newargv[0],WEXITSTATUS(status)); + finalexit = 6; /* exitvalue */ + } + } + if WIFSIGNALED(status) { + syslog(LOG_INFO,"%s was killed with signal %d",newargv[0],WTERMSIG(status)); + finalexit = 7; /* exitvalue */ + } + + syslog(LOG_INFO,"shutting down"); + cleanup(); + exit(finalexit); +} + +/* EOF */ + diff --git a/debian/README b/debian/README new file mode 100644 index 00000000000..ee7f1c37d24 --- /dev/null +++ b/debian/README @@ -0,0 +1,6 @@ +The Debian Package vmops +---------------------------- + +This is part of the Cloud.com Cloud Stack collection of packages. + + -- Manuel Amador (Rudd-O) Thu, 25 Mar 2010 15:12:06 -0700 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000000..24e490047e1 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,65 @@ +cloud (2.1.97) unstable; urgency=low + + * Bumping version number for next Cloud.com release + + -- Rudd-O Sat, 31 Jul 2010 19:09:54 -0700 + +cloud (2.0.97) unstable; urgency=low + + * Bumping version number for next Cloud.com release + + -- Rudd-O Mon, 12 Jul 2010 14:01:41 -0700 + +cloud (2.0.2) unstable; urgency=low + + * Bumping version number for next Cloud.com release + + -- Rudd-O Fri, 2 Jul 2010 19:55:41 -0700 + +cloud (2.0.0) unstable; urgency=low + + * Bumping version number for next Cloud.com release + + -- Rudd-O Tue, 15 Jun 2010 12:10:05 -0700 + +cloud (1.9.14) unstable; urgency=low + + * Bumping version number for next Cloud.com release + + -- Rudd-O Tue, 11 May 2010 14:46:14 -0700 + +cloud (1.9.12) unstable; urgency=low + + * Bumping version number for Cloud.com RC4 release + + -- Rudd-O Mon, 3 May 2010 14:46:14 -0700 + +cloud (1.9.11) unstable; urgency=low + + * Bumping version number for Cloud.com release + + -- Rudd-O Fri, 30 Apr 2010 15:08:38 -0700 + +cloud (1.9.10) unstable; urgency=low + + * Bumping version number for FOSS release + + -- Rudd-O Wed, 28 Apr 2010 22:09:48 -0700 + +cloud (1.9.9) unstable; urgency=low + + * Bumping version number for new build prereleases + + -- Rudd-O Mon, 12 Apr 2010 17:39:44 -0700 + +cloud (1.9.8) unstable; urgency=low + + * Branching RC3 + + -- Rudd-O Mon, 05 Apr 2010 12:46:06 -0700 + +cloud (1.9.7) unstable; urgency=low + + * Initial Release. + + -- Rudd-O Thu, 25 Mar 2010 15:12:06 -0700 diff --git a/debian/cloud-agent-libs.install b/debian/cloud-agent-libs.install new file mode 100644 index 00000000000..538f0686fac --- /dev/null +++ b/debian/cloud-agent-libs.install @@ -0,0 +1 @@ +/usr/share/java/cloud-agent.jar diff --git a/debian/cloud-agent-scripts.config b/debian/cloud-agent-scripts.config new file mode 100644 index 00000000000..e69de29bb2d diff --git a/debian/cloud-agent-scripts.install b/debian/cloud-agent-scripts.install new file mode 100644 index 00000000000..48058f0e9f0 --- /dev/null +++ b/debian/cloud-agent-scripts.install @@ -0,0 +1,2 @@ +/usr/lib/cloud/agent/scripts/* +/usr/lib/cloud/agent/vms/systemvm.zip diff --git a/debian/cloud-agent-scripts.postinst b/debian/cloud-agent-scripts.postinst new file mode 100644 index 00000000000..c4255780d4f --- /dev/null +++ b/debian/cloud-agent-scripts.postinst @@ -0,0 +1,13 @@ +#!/bin/sh -e + +case "$1" in + configure) + if ! id cloud > /dev/null 2>&1 ; then + adduser --system --home /var/lib/cloud/management --no-create-home \ + --group --disabled-password --shell /bin/sh cloud + # update me in cloud-usage.postinst as well + fi + ;; +esac + +#DEBHELPER# diff --git a/debian/cloud-agent.config b/debian/cloud-agent.config new file mode 100644 index 00000000000..e69de29bb2d diff --git a/debian/cloud-agent.install b/debian/cloud-agent.install new file mode 100644 index 00000000000..5a053116690 --- /dev/null +++ b/debian/cloud-agent.install @@ -0,0 +1,12 @@ +/etc/cloud/agent/agent.properties +/etc/cloud/agent/developer.properties.template +/etc/cloud/agent/environment.properties +/etc/cloud/agent/log4j-cloud.xml +/etc/init.d/cloud-agent +/usr/bin/agent-runner +/usr/bin/cloud-setup-agent +/usr/lib/cloud/agent/css +/usr/lib/cloud/agent/ui +/usr/lib/cloud/agent/js +/usr/lib/cloud/agent/images +/var/log/cloud/agent diff --git a/debian/cloud-agent.postinst b/debian/cloud-agent.postinst new file mode 100644 index 00000000000..934ee02efed --- /dev/null +++ b/debian/cloud-agent.postinst @@ -0,0 +1,18 @@ +#!/bin/sh -e + +case "$1" in + configure) + + for i in /var/log/cloud/agent + do + chmod 0770 $i + done + + if [ "$2" = "" ] ; then # no recently configured version, this is a first install + /usr/sbin/update-rc.d cloud-agent defaults || true + fi + + ;; +esac + +#DEBHELPER# \ No newline at end of file diff --git a/debian/cloud-client-ui.install b/debian/cloud-client-ui.install new file mode 100644 index 00000000000..0b03235469b --- /dev/null +++ b/debian/cloud-client-ui.install @@ -0,0 +1,2 @@ +/usr/share/cloud/management/webapps/client/* + diff --git a/debian/cloud-client.config b/debian/cloud-client.config new file mode 100644 index 00000000000..e69de29bb2d diff --git a/debian/cloud-client.install b/debian/cloud-client.install new file mode 100644 index 00000000000..7c973ec5adf --- /dev/null +++ b/debian/cloud-client.install @@ -0,0 +1,33 @@ +/etc/cloud/management/catalina.policy +/etc/cloud/management/catalina.properties +/etc/cloud/management/commands.properties +/etc/cloud/management/components.xml +/etc/cloud/management/context.xml +/etc/cloud/management/db.properties +/etc/cloud/management/environment.properties +/etc/cloud/management/ehcache.xml +/etc/cloud/management/log4j-cloud.xml +/etc/cloud/management/logging.properties +/etc/cloud/management/server.xml +/etc/cloud/management/tomcat6.conf +/etc/cloud/management/classpath.conf +/etc/cloud/management/tomcat-users.xml +/etc/cloud/management/web.xml +/etc/cloud/management/Catalina +/etc/cloud/management/Catalina/localhost +/etc/cloud/management/Catalina/localhost/client +/etc/init.d/cloud-management +/usr/share/cloud/management/bin +/usr/share/cloud/management/conf +/usr/share/cloud/management/lib +/usr/share/cloud/management/logs +/usr/share/cloud/management/temp +/usr/share/cloud/management/work +/var/cache/cloud/management +/var/cache/cloud/management/work +/var/cache/cloud/management/temp +/var/log/cloud/management +/var/lib/cloud/mnt +/var/lib/cloud/management +/usr/bin/cloud-setup-management +/usr/bin/cloud-update-xenserver-licenses \ No newline at end of file diff --git a/debian/cloud-client.postinst b/debian/cloud-client.postinst new file mode 100644 index 00000000000..ce3ebc3da6d --- /dev/null +++ b/debian/cloud-client.postinst @@ -0,0 +1,35 @@ +#!/bin/sh -e + +case "$1" in + configure) + if ! id cloud > /dev/null 2>&1 ; then + adduser --system --home /var/lib/cloud/management --no-create-home \ + --group --disabled-password --shell /bin/sh cloud + # update me in all the .postinst that you can find me in, as well + fi + + for i in /var/lib/cloud/mnt /var/cache/cloud/management \ + /var/cache/cloud/management/work /var/cache/cloud/management/temp \ + /var/log/cloud/management /etc/cloud/management/Catalina \ + /etc/cloud/management/Catalina/localhost /var/lib/cloud/management /etc/cloud/management/Catalina/localhost/client + do + chmod 0770 $i + chgrp cloud $i + done + + test -f /var/lib/cloud/management/.ssh/id_rsa || su - cloud -c 'yes "" | ssh-keygen -t rsa -q -N ""' < /dev/null + + for i in /etc/cloud/management/db.properties + do + chmod 0640 $i + chgrp cloud $i + done + + if [ "$2" = "" ] ; then # no recently configured version, this is a first install + /usr/sbin/update-rc.d cloud-management defaults || true + fi + + ;; +esac + +#DEBHELPER# diff --git a/debian/cloud-console-proxy.config b/debian/cloud-console-proxy.config new file mode 100644 index 00000000000..e69de29bb2d diff --git a/debian/cloud-console-proxy.install b/debian/cloud-console-proxy.install new file mode 100644 index 00000000000..65af05fcc46 --- /dev/null +++ b/debian/cloud-console-proxy.install @@ -0,0 +1,9 @@ +/usr/share/java/cloud-console*.jar +/etc/cloud/console-proxy/agent.properties +/etc/cloud/console-proxy/consoleproxy.properties +/etc/cloud/console-proxy/log4j-cloud.xml +/etc/init.d/cloud-console-proxy +/usr/bin/console-proxy-runner +/usr/bin/cloud-setup-console-proxy +/usr/lib/cloud/console-proxy/* +/var/log/cloud/console-proxy diff --git a/debian/cloud-console-proxy.postinst b/debian/cloud-console-proxy.postinst new file mode 100644 index 00000000000..ea727474934 --- /dev/null +++ b/debian/cloud-console-proxy.postinst @@ -0,0 +1,18 @@ +#!/bin/sh -e + +case "$1" in + configure) + + for i in /var/log/cloud/console-proxy + do + chmod 0770 $i + done + + if [ "$2" = "" ] ; then # no recently configured version, this is a first install + /usr/sbin/update-rc.d cloud-console-proxy defaults || true + fi + + ;; +esac + +#DEBHELPER# \ No newline at end of file diff --git a/debian/cloud-core.install b/debian/cloud-core.install new file mode 100644 index 00000000000..9993ce94574 --- /dev/null +++ b/debian/cloud-core.install @@ -0,0 +1,2 @@ +/usr/share/java/cloud-core.jar + diff --git a/debian/cloud-daemonize.install b/debian/cloud-daemonize.install new file mode 100644 index 00000000000..8ffbb222fe0 --- /dev/null +++ b/debian/cloud-daemonize.install @@ -0,0 +1,2 @@ +/usr/bin/cloud-daemonize + diff --git a/debian/cloud-deps.install b/debian/cloud-deps.install new file mode 100644 index 00000000000..9484d1a65e9 --- /dev/null +++ b/debian/cloud-deps.install @@ -0,0 +1,17 @@ +/usr/share/java/cloud-commons-codec-1.4.jar +/usr/share/java/cloud-apache-log4j-extras-1.0.jar +/usr/share/java/cloud-backport-util-concurrent-3.0.jar +/usr/share/java/cloud-ehcache.jar +/usr/share/java/cloud-email.jar +/usr/share/java/cloud-gson-1.3.jar +/usr/share/java/cloud-httpcore-4.0.jar +/usr/share/java/cloud-jna.jar +/usr/share/java/cloud-junit-4.8.1.jar +/usr/share/java/cloud-libvirt-0.4.5.jar +/usr/share/java/cloud-log4j.jar +/usr/share/java/cloud-trilead-ssh2-build213.jar +/usr/share/java/cloud-cglib.jar +/usr/share/java/cloud-mysql-connector-java-5.1.7-bin.jar +/usr/share/java/cloud-xenserver-5.5.0-1.jar +/usr/share/java/cloud-xmlrpc-common-3.*.jar +/usr/share/java/cloud-xmlrpc-client-3.*.jar diff --git a/debian/cloud-management.config b/debian/cloud-management.config new file mode 100644 index 00000000000..e69de29bb2d diff --git a/debian/cloud-premium-deps.install b/debian/cloud-premium-deps.install new file mode 100644 index 00000000000..87d056b1809 --- /dev/null +++ b/debian/cloud-premium-deps.install @@ -0,0 +1,2 @@ +/usr/share/java/cloud-premium/*.jar + diff --git a/debian/cloud-premium.install b/debian/cloud-premium.install new file mode 100644 index 00000000000..8e7bba9de55 --- /dev/null +++ b/debian/cloud-premium.install @@ -0,0 +1,9 @@ +/usr/share/java/cloud-core-extras.jar +/usr/share/java/cloud-server-extras.jar +/etc/cloud/management/commands-ext.properties +/etc/cloud/management/components-premium.xml +/usr/share/cloud/setup/create-database-premium.sql +/usr/share/cloud/setup/create-schema-premium.sql +/usr/lib/cloud/agent/scripts/vm/hypervisor/xen/* +/usr/lib/cloud/agent/scripts/vm/hypervisor/xenserver/* +/usr/lib/cloud/agent/vms/systemvm-premium.zip diff --git a/debian/cloud-python.install b/debian/cloud-python.install new file mode 100644 index 00000000000..b4b60b822ac --- /dev/null +++ b/debian/cloud-python.install @@ -0,0 +1 @@ +/usr/lib/python*/dist-packages/cloud* diff --git a/debian/cloud-server.install b/debian/cloud-server.install new file mode 100644 index 00000000000..f21f0917924 --- /dev/null +++ b/debian/cloud-server.install @@ -0,0 +1,2 @@ +/usr/share/java/cloud-server.jar +/etc/cloud/server/* diff --git a/debian/cloud-setup.install b/debian/cloud-setup.install new file mode 100644 index 00000000000..542cf84199e --- /dev/null +++ b/debian/cloud-setup.install @@ -0,0 +1,14 @@ +/usr/bin/cloud-setup-databases +/usr/bin/cloud-migrate-databases +/usr/share/cloud/setup/create-database.sql +/usr/share/cloud/setup/create-index-fk.sql +/usr/share/cloud/setup/create-schema.sql +/usr/share/cloud/setup/server-setup.sql +/usr/share/cloud/setup/templates.kvm.sql +/usr/share/cloud/setup/templates.xenserver.sql +/usr/share/cloud/setup/deploy-db-dev.sh +/usr/share/cloud/setup/server-setup.xml +/usr/share/cloud/setup/data-20to21.sql +/usr/share/cloud/setup/index-20to21.sql +/usr/share/cloud/setup/postprocess-20to21.sql +/usr/share/cloud/setup/schema-20to21.sql diff --git a/debian/cloud-test.install b/debian/cloud-test.install new file mode 100644 index 00000000000..37efeaeca03 --- /dev/null +++ b/debian/cloud-test.install @@ -0,0 +1,6 @@ +/usr/bin/cloud-run-test +/usr/share/java/cloud-test.jar +/var/lib/cloud/test/* +/usr/lib/cloud/test/* +/etc/cloud/test/* + diff --git a/debian/cloud-usage.install b/debian/cloud-usage.install new file mode 100644 index 00000000000..902f42910ff --- /dev/null +++ b/debian/cloud-usage.install @@ -0,0 +1,7 @@ +/usr/share/java/cloud-usage.jar +/etc/init.d/cloud-usage +/usr/bin/usage-runner +/var/log/cloud/usage +/etc/cloud/usage/usage-components.xml +/etc/cloud/usage/log4j-cloud_usage.xml +/etc/cloud/usage/db.properties diff --git a/debian/cloud-usage.postinst b/debian/cloud-usage.postinst new file mode 100644 index 00000000000..809d624a4af --- /dev/null +++ b/debian/cloud-usage.postinst @@ -0,0 +1,31 @@ +#!/bin/sh -e + +case "$1" in + configure) + + if ! id cloud > /dev/null 2>&1 ; then + adduser --system --home /var/lib/cloud/management --no-create-home \ + --group --disabled-password --shell /bin/sh cloud + # update me in cloud-client.postinst as well + fi + + for i in /var/log/cloud/usage + do + chmod 0770 $i + chgrp cloud $i + done + + for i in /etc/cloud/usage/db.properties + do + chmod 0640 $i + chgrp cloud $i + done + + if [ "$2" = "" ] ; then # no recently configured version, this is a first install + /usr/sbin/update-rc.d cloud-usage defaults || true + fi + + ;; +esac + +#DEBHELPER# \ No newline at end of file diff --git a/debian/cloud-utils.install b/debian/cloud-utils.install new file mode 100644 index 00000000000..14204856b57 --- /dev/null +++ b/debian/cloud-utils.install @@ -0,0 +1,5 @@ +/usr/share/java/cloud-utils.jar +/usr/share/java/cloud-api.jar +/usr/share/doc/cloud/sccs-info +/usr/share/doc/cloud/version-info +/usr/share/doc/cloud/configure-info diff --git a/debian/cloud-vnet.install b/debian/cloud-vnet.install new file mode 100644 index 00000000000..8a894c7d4c8 --- /dev/null +++ b/debian/cloud-vnet.install @@ -0,0 +1,3 @@ +/usr/sbin/cloud-vn +/usr/sbin/cloud-vnetd +/etc/init.d/cloud-vnetd diff --git a/debian/cloud-vnet.postinst b/debian/cloud-vnet.postinst new file mode 100644 index 00000000000..02c33c835aa --- /dev/null +++ b/debian/cloud-vnet.postinst @@ -0,0 +1,13 @@ +#!/bin/sh -e + +case "$1" in + configure) + + if [ "$2" = "" ] ; then # no recently configured version, this is a first install + /usr/sbin/update-rc.d cloud-vnet defaults || true + fi + + ;; +esac + +#DEBHELPER# \ No newline at end of file diff --git a/debian/compat b/debian/compat new file mode 100644 index 00000000000..7f8f011eb73 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100644 index 00000000000..ef4e379e20d --- /dev/null +++ b/debian/control @@ -0,0 +1,184 @@ +Source: cloud +Section: libs +Priority: extra +Maintainer: Manuel Amador (Rudd-O) +Build-Depends: debhelper (>= 7), openjdk-6-jdk, tomcat6, libws-commons-util-java, libcommons-dbcp-java, libcommons-collections-java, libcommons-httpclient-java, libservlet2.5-java +Standards-Version: 3.8.1 +Homepage: http://techcenter.cloud.com/software/cloudstack + +Package: cloud-deps +Provides: vmops-deps +Conflicts: vmops-deps +Replaces: vmops-deps +Architecture: any +Depends: openjdk-6-jre +Description: Cloud.com library dependencies + This package contains a number of third-party dependencies + not shipped by distributions, required to run the Cloud.com + Cloud Stack. + +Package: cloud-utils +Provides: vmops-utils +Conflicts: vmops-utils +Replaces: vmops-utils +Architecture: any +Depends: openjdk-6-jre +Description: Cloud.com utility library + The Cloud.com utility libraries provide a set of Java classes used + in the Cloud.com Cloud Stack. + +Package: cloud-client-ui +Provides: vmops-client-ui +Conflicts: vmops-client-ui +Replaces: vmops-client-ui +Architecture: any +Depends: openjdk-6-jre, cloud-client (= ${source:Version}) +Description: Cloud.com management server UI + The Cloud.com management server is the central point of coordination, + management, and intelligence in the Cloud.com Cloud Stack. This package + is a requirement of the cloud-client package, which installs the + Cloud.com management server. + +Package: cloud-server +Provides: vmops-server +Conflicts: vmops-server +Replaces: vmops-server +Architecture: any +Depends: openjdk-6-jre, cloud-utils (= ${source:Version}), cloud-core (= ${source:Version}), cloud-deps (= ${source:Version}), libservlet2.5-java +Description: Cloud.com server library + The Cloud.com server libraries provide a set of Java classes used + in the Cloud.com Cloud Stack. + +Package: cloud-vnet +Provides: vmops-vnet +Conflicts: vmops-vnet +Replaces: vmops-vnet +Architecture: any +Depends: cloud-daemonize (= ${source:Version}), cloud-python (= ${source:Version}), python, bridge-utils, net-tools +Description: Cloud.com-specific virtual network daemon + The Cloud.com virtual network daemon manages virtual networks used in the + Cloud.com Cloud Stack. + +Package: cloud-agent-scripts +Provides: vmops-agent-scripts, vmops-console, cloud-console, vmops-console-proxy +Conflicts: vmops-agent-scripts, vmops-console, cloud-console, vmops-console-proxy +Replaces: vmops-agent-scripts, vmops-console, cloud-console, vmops-console-proxy +Architecture: any +Depends: openjdk-6-jre, python, bash, bzip2, gzip, unzip, nfs-common, openssh-client +Description: Cloud.com agent scripts + The Cloud.com agent is in charge of managing shared computing resources in + a Cloud.com Cloud Stack-powered cloud. Install this package if this computer + will participate in your cloud -- this is a requirement for the Cloud.com + agent. + +Package: cloud-daemonize +Provides: vmops-daemonize +Conflicts: vmops-daemonize +Replaces: vmops-daemonize +Architecture: any +Description: Cloud.com daemonization utility + This package contains a program that daemonizes the specified + process. The Cloud.com Cloud Stack uses this to start the agent + as a service. + +Package: cloud-premium-deps +Provides: vmops-premium-deps +Conflicts: vmops-premium-deps +Replaces: vmops-premium-deps +Architecture: any +Depends: openjdk-6-jre +Description: Cloud.com premium library dependencies + This package contains the certified software components required to run + the premium edition of the Cloud.com Cloud Stack. + +Package: cloud-core +Provides: vmops-core +Conflicts: vmops-core +Replaces: vmops-core +Architecture: any +Depends: openjdk-6-jre, cloud-deps (= ${source:Version}), cloud-utils (= ${source:Version}) +Description: Cloud.com core library + The Cloud.com core libraries provide a set of Java classes used + in the Cloud.com Cloud Stack. + +Package: cloud-test +Provides: vmops-test +Conflicts: vmops-test +Replaces: vmops-test +Architecture: any +Depends: openjdk-6-jre, wget, cloud-utils (= ${source:Version}), cloud-deps (= ${source:Version}) +Description: Cloud.com test suite + The Cloud.com test package contains a suite of automated tests + that the very much appreciated QA team at Cloud.com constantly + uses to help increase the quality of the Cloud Stack. + +Package: cloud-client +Provides: vmops-client +Conflicts: vmops-client +Replaces: vmops-client +Architecture: any +Depends: openjdk-6-jre, cloud-deps (= ${source:Version}), cloud-utils (= ${source:Version}), cloud-server (= ${source:Version}), cloud-client-ui (= ${source:Version}), cloud-setup (= ${source:Version}), cloud-agent-scripts (= ${source:Version}), cloud-python (= ${source:Version}), tomcat6, libws-commons-util-java, libcommons-dbcp-java, libcommons-collections-java, libcommons-httpclient-java, sysvinit-utils, chkconfig, sudo, jsvc, python-mysqldb, python-paramiko, augeas-tools +Description: Cloud.com client + The Cloud.com management server is the central point of coordination, + management, and intelligence in the Cloud.com Cloud Stack. This package + is required for the management server to work. + +Package: cloud-setup +Provides: vmops-setup +Conflicts: vmops-setup +Replaces: vmops-setup +Architecture: any +Depends: openjdk-6-jre, python, cloud-utils (= ${source:Version}), mysql-client, cloud-deps (= ${source:Version}), cloud-server (= ${source:Version}), cloud-python (= ${source:Version}), python-mysqldb +Description: Cloud.com client + The Cloud.com setup tools let you set up your Management Server and Usage Server. + +Package: cloud-python +Architecture: any +Depends: python +Description: Cloud.com Python library + The Cloud.com Python library contains a few Python modules that the + CloudStack uses. + +Package: cloud-agent-libs +Architecture: any +Depends: openjdk-6-jre, cloud-utils (= ${source:Version}), cloud-core (= ${source:Version}), cloud-deps (= ${source:Version}), libcommons-httpclient-java, libcommons-collections-java, libcommons-dbcp-java, libcommons-pool-java, libcommons-logging-java +Description: Cloud.com agent libraries + The Cloud.com agent libraries are used by the Cloud Agent and the Cloud + Console Proxy. + +Package: cloud-agent +Provides: vmops-agent +Conflicts: vmops-agent +Replaces: vmops-agent +Architecture: any +Depends: openjdk-6-jre, cloud-utils (= ${source:Version}), cloud-core (= ${source:Version}), cloud-deps (= ${source:Version}), python, cloud-python (= ${source:Version}), cloud-agent-libs (= ${source:Version}), cloud-agent-scripts (= ${source:Version}), cloud-vnet (= ${source:Version}), libcommons-httpclient-java, libcommons-collections-java, libcommons-dbcp-java, libcommons-pool-java, libcommons-logging-java, libvirt0, cloud-daemonize, sysvinit-utils, chkconfig, qemu-kvm, libvirt-bin, cgroup-bin, augeas-tools, uuid-runtime, rsync, grep, iproute +Description: Cloud.com agent + The Cloud.com agent is in charge of managing shared computing resources in + a Cloud.com Cloud Stack-powered cloud. Install this package if this computer + will participate in your cloud. + +Package: cloud-console-proxy +Architecture: any +Depends: openjdk-6-jre, cloud-utils (= ${source:Version}), cloud-core (= ${source:Version}), cloud-deps (= ${source:Version}), cloud-agent-libs (= ${source:Version}), python, cloud-python (= ${source:Version}), libcommons-httpclient-java, libcommons-collections-java, libcommons-dbcp-java, libcommons-pool-java, libcommons-logging-java, cloud-daemonize, sysvinit-utils, chkconfig, augeas-tools, uuid-runtime, grep, iproute +Description: Cloud.com console proxy + The Cloud.com console proxy is the service in charge of granting console + access into virtual machines managed by the Cloud.com CloudStack. + +Package: cloud-premium +Provides: vmops-premium, cloud-premium-plugin-zynga, cloud-premium-vendor-zynga +Conflicts: vmops-premium, cloud-premium-plugin-zynga, cloud-premium-vendor-zynga +Replaces: vmops-premium, cloud-premium-plugin-zynga, cloud-premium-vendor-zynga +Architecture: any +Depends: openjdk-6-jre, cloud-utils (= ${source:Version}), cloud-premium-deps (= ${source:Version}) +Description: Cloud.com premium components + The Cloud.com premium components expand the range of features on your cloud stack. + +Package: cloud-usage +Provides: vmops-usage +Conflicts: vmops-usage +Replaces: vmops-usage +Architecture: any +Depends: openjdk-6-jre, cloud-utils (= ${source:Version}), cloud-core (= ${source:Version}), cloud-deps (= ${source:Version}), cloud-server (= ${source:Version}), cloud-premium (= ${source:Version}), cloud-daemonize (= ${source:Version}), cloud-setup (= ${source:Version}), cloud-client (= ${source:Version}) +Description: Cloud.com usage monitor + The Cloud.com usage monitor provides usage accounting across the entire cloud for + cloud operators to charge based on usage parameters. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 00000000000..392d7a8dd49 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,46 @@ +Copyright: + + + +License: + + This program is dual-licensed. + + For the free software portions: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. These portions -- clearly marked + throughout the program sources -- are also distributed under the + Cloud.com Software License 1.1. + + For the proprietary portions: these portions are made available to you + on the terms of the Cloud.com Software License 1.1. + + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +On Debian systems, the complete text of the GNU General +Public License version 3 can be found in `/usr/share/common-licenses/GPL-3'. + +-------------------------------------------------------------------------------- + +This package was debianized by: + + Manuel Amador (Rudd-O) on Thu, 25 Mar 2010 15:12:06 -0700 + +It was downloaded from: + + + +The Debian packaging is: + + Copyright (C) 2010 Cloud.com Inc. + +and is licensed under the GPL version 3, see above. + +Note: if your source package contains a folder premium/ or a folder usage/, +then this program is being distributed to you under the terms of the CSL 1.1. diff --git a/debian/dirs b/debian/dirs new file mode 100644 index 00000000000..ca882bbb785 --- /dev/null +++ b/debian/dirs @@ -0,0 +1,2 @@ +usr/bin +usr/sbin diff --git a/debian/rules b/debian/rules new file mode 100755 index 00000000000..c99b62b85a7 --- /dev/null +++ b/debian/rules @@ -0,0 +1,129 @@ +#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. +# +# Modified to make a template file for a multi-binary package with separated +# build-arch and build-indep targets by Bill Allombert 2001 + +# Uncomment this to turn on verbose mode. +export DH_VERBOSE=1 + +# This has to be exported to make some magic below work. +export DH_OPTIONS + + + + + + +configure: configure-stamp +configure-stamp: + dh_testdir + # Add here commands to configure the package. + ./waf configure --prefix=/usr --libdir=/usr/lib --bindir=/usr/bin --javadir=/usr/share/java --sharedstatedir=/var/lib --localstatedir=/var --sysconfdir=/etc --mandir=/usr/share/man --libexecdir=/usr/bin --with-tomcat=/usr/share/tomcat6 --tomcat-user=cloud --fast + ./waf showconfig + touch configure-stamp + + +#Architecture +# build: build-arch build-indep +build: build-arch + +build-arch: build-arch-stamp +build-arch-stamp: configure-stamp + + # Add here commands to compile the arch part of the package. + ./waf build --build-number=$(BUILDNUMBER) + touch $@ + +# build-indep: build-indep-stamp +# build-indep-stamp: configure-stamp +# +# # Add here commands to compile the indep part of the package. +# #$(MAKE) doc +# touch $@ +# +clean: + dh_testdir + dh_testroot + rm -f build-arch-stamp build-indep-stamp configure-stamp + + # Add here commands to clean up after the build process. + ./waf distclean + + dh_clean + +install: install-arch +# install: install-indep install-arch +# install-indep: +# dh_testdir +# dh_testroot +# dh_prep -i +# dh_installdirs -i +# +# # Add here commands to install the indep part of the package into +# # debian/-doc. +# #INSTALLDOC# +# +# dh_install -i + +install-arch: + dh_testdir + dh_testroot + dh_prep -s + dh_installdirs -s + + # Add here commands to install the arch part of the package into + # debian/tmp. + # we put the build number again here, otherwise state checking will cause an almost-full recompile + ./waf install --destdir=$(CURDIR)/debian/tmp install --nochown --build-number=$(BUILDNUMBER) + + dh_install -s +# Must not depend on anything. This is to be called by +# binary-arch/binary-indep +# in another 'make' thread. +binary-common: + dh_testdir + dh_testroot + dh_installchangelogs + dh_installdocs -A README INSTALL HACKING README.html +# dh_installexamples +# dh_installmenu +# dh_installdebconf +# dh_installlogrotate +# dh_installemacsen +# dh_installpam +# dh_installmime +# dh_python +# dh_installinit +# dh_installcron +# dh_installinfo + dh_installman + dh_link + dh_strip + dh_compress + dh_fixperms +# dh_perl + dh_makeshlibs + dh_installdeb + dh_shlibdeps + dh_gencontrol + dh_md5sums + dh_builddeb +# Build architecture independant packages using the common target. +# binary-indep: build-indep install-indep +# $(MAKE) -f debian/rules DH_OPTIONS=-i binary-common + +# Build architecture dependant packages using the common target. +binary-arch: build-arch install-arch + $(MAKE) -f debian/rules DH_OPTIONS=-s binary-common + +# binary: binary-arch binary-indep +binary: binary-arch +# .PHONY: build clean binary-indep binary-arch binary install install-indep install-arch configure +.PHONY: build clean binary-arch binary install install-arch configure diff --git a/deps/cloud-apache-log4j-extras-1.0.jar b/deps/cloud-apache-log4j-extras-1.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..c2a866db405678a836847222d93a230a1a0c812d GIT binary patch literal 153863 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%0D66f>KUENH43FG28AOVz+wJlQ=hKr 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 literal 0 HcmV?d00001 diff --git a/deps/cloud-backport-util-concurrent-3.0.jar b/deps/cloud-backport-util-concurrent-3.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..99f9b9c6a3e655702a827bea183e327589ba0912 GIT binary patch literal 327810 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

oj}8*<66_PVBdL7 z9RIf}VYBi!U`7J*^HI~YiHEvLMB?D5;m~75*7Lr^K8z$iMF!-p>EvU_39jzb=p+2W z+^=CXexD#ITUS#v*TeM}vbXp3?+lo5}p!n0&bNH;esYL#ucDn*5dZU>jq5gHGSs%#31B*#xI%^=AD{fCg-m0 z3X(5fML*dUPaEr{d;eZY^+?2*^j(O~#E-C8-U|V%a=icK_J4ork z=95=`L{^b4&P%|4=zMfCGNU6m(m521eG3n%yPJ{u?=Q7j|HGW;`PdxBb2ibL2C3K@mT!t(QE#~UEkCPiT7^;}xU^r^WZ4?ykCcph>l_$XRLh~T_7aVS2F6}^ zuqZ2-Kjiq1#h(d_U8Tvu7Y^~z|HX<%wfIbT9b z>-Gj6_>7n|Kz2_yjsX@Jt0iTG8iKWys8G}twdx2K;GB}rctsI8dj@-hSz%~^&9yKd zqwgVH<_t~Gk3K&%2qc#aABHr5b83Q@S}tR+6YnAOIQyeh&<%cQklUMH>Jn2Vuth-~ zSF%f8ga{WgcT4sX_lZ$|{eheZYwg1C+%^|G!SIimj1>i<$X< zPi%=B*ct=8y8i`6VmoC)1YkvKg;q$V;`}2J^1eZb@{%HuA|r#v#`JSpnXpNls^0C~ zSfP41ADWG$#nA%gHD)Qhz^U6~X<-ELnEi9?IXqJZ zyI$T=*=^6FncIdz8yyvc4>?pVr8}Ww6kPX9F?OhFZ@|JW(Ma4qiIO6n>#O7t#|l?@ z?6us5w}O;yXqfz>N_HzUxEcUN^UB8t z1G#peIVMyKXJiU#HTLYcME%u~40>12id+X4=?yPlRNfSdE2tOOB=#AEq4=Hj<q`0EZ?&q;Y%}}D zWgTPL1~>0f^q3{1s>u(@2x?NrrOH;+hgF6&k%`nGx%wXzTyhdMz;&odC{!yF_js%L{bhW}sZY`) z32Y*ydkgFM|Udvqm(-7O|6l~{84593SZ zjrG+T;qtyCaov)oo0MR|qLvz05BU!m;A|Y(= z*zT4?K3TgA4@ofe&_C^*yz6pck6LuP#Yfq!0|Sp*key|Y+wBwYP?X#+F78hOY6(1U z@OKbyh^;2<##&T;kTBNYXQ0kgwM|uCK zqYagP{bkoVyadI50d{=~&{O&!?>ghZBCev09iRc}BfDVRXnp)slNUM(7-(L#f~rOt zsSFueQ8^H}n=3{_n{{jaQFQzlB?RamVB4X8K`^*F5-u+$nDN+|=H!|^E^h+}GC2Dl za4H)z^-B^}-P7&V0RI`4;k*2Cx@4Dk;@uhsv9E;urEE+- zWnC#n$Aaibn$V}Ivu@L`tb9++d$jJl>S#_XF&-1XFkRVE)6O^|J`7a|KX6qcCCuJy zO9l$M<7lZCW>~GM4_E5Z5UgvrZWvM?umxKM&GM~ddmHS?7(vZYP&UTJ8}`0LSU7GI za~iS`10UUQ1Gd3g1=trqx{MyuieQz$n(sU4p>i0s&zQ^br|9E4sNnD|egHh=Nk9%t zOW{rz{RAu*#)y#X#QeS(;haJqP0gu&n1WUEbw*@vmyZ{T_y|t=cGc>F?8>~uInIoQ zXs*A1H@1^$46~wYC}NTgwTX~vk10S<{$+Gx8FL_KJXUgpB?Z=6*ezsDP!*^$uQLE| zGWdY-pV{Owbj!~Ya6kqDVL{=48Wv=~=YG7{T!+R0?H@8ohTEQLhWF3q7;~Mx#8{L zJ2J|*06&OghopxA1n&!&9^D-&%!vbb>?)mU6^3YoN#DN|5zB(|mE(X-6$ca%f1{K9 z@A~PV@m|TrR^G(X^e;QBG$D%$*jbi=*;!@;Bt!))q*()HPkvO7upknW1gHXna2O03 zIS!>29k>sIT{Z1z2wG|3aI#?*s?C=uD!&2u$~1F*3g>y1R@qIjnRb=dM}XE8LY}_{ zN_Cqtff-RqSNKGm;f^J-CVdtmgf!23P&ll0VwM;xZ$+pT^miuv&0aW=eOQ<8+}{1O zni0qrZ$yq4q`)PZ(tJ0D^}B#TQU?ZJNZlfgI$PnZDULKlU*sfInPf;y?vyMj-jnuh z#jWkI#4Wfc6>#QWDNDZC0lqY(f)xMcNQAbah-n?(uvd2}IX1vC6HX)xA4#e!Ykdi^ z+j7C>@H_gPWyGfgDY(A-(wf5+8D_>6 zKq-Bq7sJsnxu(q|6#QgVU*uVPSp=+fGmUlXfB;H|moCX&=?A{*J9>wGgu$UWr5IKo z@+LFAfdMi}<4doII(vXC%)&97{X{>ktrmMYJWRGFc|q>3`b>Df41`Ba)ymV-Qu}u% z)UQN?de^+92j5HgTBQe_+L%aokudl592Xrt5vPEtf!;f3vE-oQxQwNJG+7Mp6e`@P z=T?rhVWK!Cq2hrdfGwhmNb#$`*d5`vbenT4R!OWPj7xlBF+3_XSg5qEfq;vaRJ19r zxbdWrXEV)xe>k4SYdOrZLTh>FvQue}(!CN!f*PB~%!$Q!X1T*{|ATG9hp?!7+EZSF z#-LYt%%OLx#b2WP+1A^%lQ+`5>&Z86vq^v+>Vx7Dx6D2m^XxN{%d;?S?pzREu+I=; zj{)d5LjZm-kjE{7XqEuo6c2kWHHFcibs1L(3w)vrA7c~dHNAyb%!eij*h?x)TGO#Q zZ|saovZ}F>h}9-ziPa{dMNY{p+(NNAn%Tytn>m6(!7H--t6*jUKTc2n zHK!4#s)M&0DJPpF_3G7k`=y7;?dxt==9`s0TNpaKK#_xTP$eNsFhY*EaZnmUZPxk- zWgYTO)uL`gtxcUyk95m57Gy>z-L-r8Xpf&PtAd+ z@#_x)^nDhJN0;*JXaWGw)snqvS4?10vYKx=6Wf=FP0r%EJTy0X|L9o+YYTdhc?-S( z=-1-4EMoLEb64fAn!w!{v$U8>{;#*Z4ONy&^F|1F#KlJ#UpMl2@cI(-7Gr_9T1 z9D#P5mFYYdvXOPqNn=*q7>-L}@5yUXl=SDgc=iNWZ93l-cGJPeoaBffI9%~AIt!0` zg{G7?9qI1dQBkax(nlsrVwwbth3rb9JjnRh+7oz$Q(d$p)9#@c7i@|ejj(25=-+Fs zq?yT2!6i29%#Q2QuR+n##pPq<`Cod2FQPMYk`W=Bv|0{aAd^JDKfyeAWEDxsc~P&Q zPsG5bU*<6+F&2}`_KRPzX-iGtMe^D$64B!3F5mfRr-CO>^_ESpkRmb-#Tt6>I_OihV!=-`5f0OeX3X}tU^}5%n)l#u_7a`02g6gf@0d^kR>r`BQ9xr6~bk^j*p4Gi4V=>wB2RB zJ8_T{1wjGF{w&z9eO~98K13EhZ5uEAYy(R_MjeBO^IN__@Fu$M518)u+4Pxd5p8Ih zgn7l4;1Ff(KgAxCHs*G+W`W@h@kFUiJfx4bWF%seCzCTSS!@+y+)qixEZ$Z0|3g|G zFH=voId@YIdYx;I;a+%u{UR8%!D&iAR;BOF9bbUB6hEx z(H*-6O8Lt9_6U1^ytdHl!>@l=DC#3Bzg~#n@TQqfz1DyLV;z3^x?#$UXJ|#S#I=%O zQZLVw%TqNu{)E%gU~Y7yi|T2gyo=4k=yT{G2UWnBTTK>O5IyuNzwovH^K=wzH96~k zFvlQZ$vnPf>4RU3a_hB!NE4e*%Y?WjRj$3lydF=D0xGM%$p~dm`%Rl$>>y(RdCdLu zRlW~KptS>&O#I3C9>`-;BcNCS^Z=;#O^BZBIBHA_no z3eO8->A;q=t2Be8PsNGG_1!+FJ!et!k4ch_SbHy+tZ;xoQi9zGe1e2yWTkJ<+3C1k zH;CV62nA>>%WoagA(vxboU;9{EoQn&O{Be6RQsRs-q5Qs==(JdZJf>c!NBK=CY35@uxdkX_izwr`;cl28w@frlV3BOJHn~G z4+9|!9;`i@|55wCWl=?Nfdy*mjbY_Ib%bOM4>8DpCo+bVD8Bq`nzz)C{R5UTmr391 z$zO&{hOzl}lY!%7p1R4aK5V)wM6Zgt>8xJSTDWl~7`;BduBAY)X+LfyF!6s{2=5nJYLG;ikkChRO!A;iD%)7WDUo$!I zTh#j)q`r7|l=c)wBF0%4Glj~0Xhw;=WBn1hH$-jU%qX}mg$M7Nf@WF37;7OR|JzS} z$__*q8~LRJX4sjfrWtVxm_gn8H*iae>=07N?+-jvxp@ugnqKJ)RJKM3mlDd4+QSvI ziE1~fG-0eaVH^C!8X0XzXS-c{;D%YXTbs^cCaFmm59T{x;HPVn=g={58SIe`_sqoI z@GMmW4^_=@RehD}L23(2&41ST{aWhMTr1Yvg1}AzX5&SvXD2>z-B=296@!9N6`DN} zIa=(K3Z)fAV(gR#B9i41dVvfNP4%L`zZ5m+`qz3uqrGbnI$3|t|FwU0ceXx%1wdo` z|Fc5%zpk=L{`ZBpv66?a(ZAXn2~v_E0*Iks5e}T;48vJ{A-2{}wx9x#l7XP88!n#E zW{lZ6v$6a&hy5V#v8 zLY1smH@_e7c=ey(hToBU=yYalSIV2yrAn%0u7R~HYLOa|M=v)2p5tf{K(}T+@3M}Y zi6_>3@3*VnN>G+Zn`u3VpYkPyMR#$|z?V#f$NHerVWSRk>fhh;te(N(s!ZJYGTZ=; zYPfRj(XNLNKIy9=-nf_;hYW@X^DE&>|LN6_EC$^CuI``@ekk6*HO#{b0_kWvCra(Ud+Aefx}`T3zp0!2X*)1b-- zy9DF-8R;kexeGijn4Ci3nKoAz1_mf;Cgv-ss~q>qD0CVP2vY={4bU&&@oRM2(Pl32 zu$!nW4MQnypSritvaTGwd_V3=bAhyj@dh03MHONvEKefy@j*^)uaRrYOxQy! zF~j#{qK4bFBlV@rW2manPA^13QIMpeg_m3g7$!-99Ha=$$Kk~CUda#^RAm9$cMD06 z=DKv~CJOqDq)x@b)as9>1$kT(tA>UOGc{G3Es1TlwI|^TMt@K=CMHv?wK8N4H4fE^ zb)?B-nMgGf%;3EdAcL!1I5Et{+gm&~wIwHM))i+IF!7kB2il3`f3G(4n-|jNbR-#! zJD1myUvT&^&?VO=|GtCiuqpF-p+Bbn9<0?(k{yMyFKo8_OOygzsw5|tfE3XQ=+KmH zs;bxds8NaMo^7_cLCcv(PGzy4f3Ywwl_MY{i^UApShgH`QaNQHmcp}NCu#efAwx5ny2TP(%_{N94Nm zq4yI0iw<3G;Pf!l>V)j%{m|1)&FyV4G}Qr#CQEbv!cY`<+CU083ukk8JQ{39N!DmA zI6DK@Kn}P&^{}0i(AJxnP^~r^``2G6#k(<%j%qYd*S4TnP$X2WhQJR$cbOueOj6o& zs`N{~(0el}0*p*1f_zq*aQ1ylde-LDj7?=^j5>h9p6HNOF2iUVCm1^wEtb!p`@T+W zbB<2o6|-wY=ex()p}XZy^VzGsA5{r_IBc{nRZ8qL!RCv`TYYz1 zfV_q?kNNtBgup9GK#NyM*#pt@)0G@*Cn3Wk7e%vRjro1Ro=jAQOp%UEqqAO3VLE zZ85FjgomP@ch72bW$%`%cEnS|*`V%>a>GecnjY*#ZJefCQIw!kA(M&4G)%mPi6x`~mQ?wI8 zl|9Y1cMx(UgX#;+ISBe_NtZCHRn%v`-YjcN-X4-XruRLM$-NlEuVJOyO5gkz%L4;^ zrz^bEQ0}A$lZ}&|*`?1b>vmOyD|GDFpbgP(8>#Ct_yM+h0vl(&i4vK%&8-XGSmf8R zvysK;;I&-EFCZG;!gr^BAO21{b$>b?L5Dyd5iOCj2y_gM3enWHIysv*X`0?$1qa%C zuise^?3)szVT?KwSx{nsr7Pg`2|6|d`l#)IM*IJOuJB(0^j|qiX&vAVg2=Pp;b15z zB?LqBG|yW^@LQZ4oN!TG9ElLJnNo3^b((x@NkgpDYSC9UH_m+rrRNx0Y#dZM>q5xAlE5=PhO)1vl9{$_rf(X$c z%bX}>-AbD6Pr#KI155#B-y>LGB^UBCV8^n?4f^6$e@z-YkVW5qa$~k@YXt1`Mpq<} z6}QX&m_JCD`N`Osl`>q6aoZ_PRT{xe<)Vruv{uZ4ro_nlKpB(<52LV_-~b%-IPVPc zHGK7)ZrI@{YAXHF?&BW0G09!M!H{~_?_}XK8!D0m!#w=wwP=JEAwH*v!+%M|k3pv)?5e1F} z7(XRjZD*_CDRmd@MdZ=eCK5O|ln- zzj{mZow9-gs99iLk?soA?;tQX+FB@7cFg+HaJ6~Vec6s01h}-YGiUT z`cYQan6WA2dTp%Sw`KMN_C5APb_4dxfw-uib>%Uyh8vGd3CqyOp#v%*jzV^4Q8n7t z6@TK;twGY-<9ya6tmMX5k=M!)ZK)Wb0pb&YXZ5>im2p-jL5U*Gc2=z62olATD7o41 zEqTn+6oL2AtnFV^iGtlFrLau*ejf*;nzzaEg!1~B%n_n%>6mgDMUKOne5+T#?Y>aD zzhP>6thN79{h1#GLa+_&{WEX%-Ez8mB)4F+j~WH?3GLbqZ4f zk=bN`BHx-1kOUa#Va;1`x*XFd&i{1CkY+IoJ!$%qe#2nqaZ^hn1?LRD4tnH$>^$<_ z=GgvtJk|U9Sl-&&l$E?84N47B#)agb|goKo5)rzx6J=+o-AqLag~C+WS}Nqr!goykN~#h zkpAg)(L`%t(qDIPY-=hw6J?1)W@c$#kgjbU=3p?No6U%6^7M2Y2vq%h+EUbYlCi2P zXY7xxSu~WBs-Q@uy#(X)S?bQhjD)#ln}3O^%E$nWQp{uX~*)kMpEf( z)nQT^(s5a^e`jW}yaSriu_e}XjY1SQ6Q|Ggt88d;v^lfzZ{%CIFplm1J z_l({Y(^Y4ASI6?C%Q_6ox*-`_%{$ZKIVI+5=`+o>q7-D+O*=7hn|`Dk z#zGvU`FY4v_iVgG9}mSDPmOHr)mvjGcIILZG0fl`2krC|M>sbWrJ1Aqten~EvGb(2 zi1Aw5F2<^{xpG+Iy#j#bu*bgcI`e}vA&?-kD{bQZ8*F@mWnT!ZK~({k4JGR?1f?Tp z(yVrZjBwiGq(ElVdU`xH1?Az-C??A3Jr1;^!B>qY3G?@_aB|oD6OXQfJ+p!*r=~G? zl_9$8jHqRmt^gQH+5)MeO!rDy+H|HVp%yQ6N$$3yJsa(~o0tfNpwCj32P@ zPD!`S>CMwh6v9II4F)*MGEaOOyAKvA@$76Jpi?~=5~k_^3SgmRTkzPs1K^KkA?&AJ zT*^1gQBzqK2DPzQKGs5WTyX_jU(nk&G2ScvW-OG?)_a=s_N0!zqS65 z*EpSvU`0?`llvZWr5pJP)>A7lAaGh+cxv@LazzlQ$6rUqnWJ5Gb^Y^$O@3cv44L-z zSN7ZE_MC_Ev_Q<;hfcNI5n=>{=G7ATtzMod{Kd@<2ILa>okM$1A)~g2HZDLYv%X^& zI|)pAN~oJ+Tl5=O9gNqp4DsXcHy=)a-`45@#L!jv{jhaUq$dNG+?h576{Z@<{c@v? z=qNV`;*Ah|6q-=mt=>mNF61oSep@2v{yy21A)P7<(En_~LQ8he27MGYfd#P( zm%eM~X$(ZX-`L(I1&Lh5K5I2$HQYm99h z^#ry2Szb_!ZPLiZEU!Q)CT4^RW8f2>v?^(?64I3j=wKYqTC=x8_CQV@HyZR%VL(B6 zjyQlSSMYJBPcDw5%t6FlzVeS1Rk@%_SA>mC@{fVTHcDgpyBoBPa!zfX5~1eGfrvWq zNN?EN0iT%qi=Eax!*$$`q!cqKJo%NN&v92AKIaORp)V8Lf92t@q5r-Whug*I-;^6Yg}tNKtEScgUx= zJWh$66|X&ZZJKww)p>h%cf*yw&#e!5m*u&&>Ri0vpE1VDWI41j)tH^+YL^`89>$a& z@xgd*t-m7VuVGiTu<9+gROSn>R^LHhQKhyP5cBHNJWkr!sjDrU^G*nh))xx1C!J#MH=&H8t=J$gK*A-R~+0l>EcEuuKu0TAm z*tqQ}c@IA_y-(Zl7{u;V@ahG$X^XhGRun4B-R)SHXmi;IRBn;XYYF>g&UXo=Zx`!d zDNIjn98tQx#zEa9ZR_rykFxirZj&7yH9?w z0s{ekju6SuW_PJHu*++6_hKK@Y{^;aJn9_hkRl~@cPFJS_@hfZ(a9w zaxuNldi(f!1GD$}Mr~sHgvPj|aMD`?c=DltUUe!gW~Wma9x>s+KaN>`PkKUQ0Ql8p z6}t%~t7)T6&v+2_S5kYB{V_iE-Z5f8>4ILfKdGwfQe(Xq(;?45k%gdZGG$0%hJ`q> z|Ls5;Xi1__E~bjO`5{9KdoUJM*`>_3MjpCV+}K`Y$5Zo$HT~t0;NYludw?41OU%?C z^X$W_nqbYLDbVVNSLS`9IM_QY_))-`9>)3`x8hWtn8Cz6ZHlEN(im=4)4cUedjjtL0U3N`LeHTFenj)JTj#I5v)p|SV z3-YwT*QY~|F98)460BE0wqf*Owqk8$?gW$rQO;D5;t`B=ms@QeN{u#> zWKO1zx|FCyi;|IcYqzxzRnyQc)3Y6Mf|eq&q7jBI$i-h3gAalp-4|a&_zIo^tf_eW zP6A*yAEI1qFT>Gk{%;mBb=q}GMyGlS*=~JZrNb7g>M|FDwFW1B8;{(YD6#S=&-8D| znB3el$uG2TK~D3u!&*6ulNX?z+}(^BogQV*hpZFW9(0<(NONJ>V zPP;Unt%z*Y!eva=VEdftX{@CPv%N;oR33q)Cf8ht@v-IFUuf;xjPp_x>QGQ> zH&uh|u&(b^FQ|g=0f?l+gLq%Q*%KhFZI$HDXX=Ae)DG6kFK280bpJ_ZQcmi?b_X~_ z{RG_G|BVv-|2hr-t_ZA@z60*=;i)w(G$8e*nuGS)>xJ5eA~sQxdu6Q+0_(mh8I)z0 zj1g7Og_>YksSUSH6)TlWIN5EIHD?wrW0f+!$d}ScoTa}SnY~JJE+AZ#`c}PV=@5dt za31!XtI2WlKq~=93ysi# zW5C3o&L=RVZa)O$VExhA66(QDsSP*fr~(UvJzYqyl>@COBnY2sFEBtvJIvl%gtVK$ z{MjP>5u^QDVBJlEPd70_McGriD{%7ZMM+x;K$K5lsCHvx&=-F0`KQv||MvFdOp_$u~X0zy$RoLt8mY16RS*nZG&^(Y{) zUUV6cyWA>?-t=O(ea28!Bs<=f^zvX$FpNmvF_}8dr`2=J_mhUo5KBEy1V!l8b;%53 zHDF32M5m_H%MQT{j$4}v19L@-SbD70<7`u-OTqN87|}s3woK@U41G26SM995BxyN% zKjH?IE&5R-?)_Ps8G^=KT?DgsN0wM1!C21s6k8KxJvE-sV8cq>-gZ#se^_;rN_c}b zq2`z$K*QSZVroab-dyL}MG?n>PU^DD&`XNX^ zWMfS$r!jqQoG(tvy!LxbVTj~lttCCNBT>$ozzjky(zn63JdNomwMSWI#q@!IMI~|g z0B}$yHxzPBzq;YWnL8+i3wO*9=l5@BpliV2*6mFbEf}*{&J|K8Hw>3bU6Th2_TU2U zTX4m3?EqT0c2a^D$4fWxTgg?gDqW@fKhw}xv_z~Az|&u#332)QJ1J|9pBa1$_I(ZNI+(+1e>5{OJ_-swK-Evi;=T6f)T(5QDH5aw?#`4xZ;)>@GB8zJ6XR5;I=!pOn)=y`vi|*d*yK{QRpmF1YaD5C z5q}OZW0{Rr(iyg+^x=tX$JH{E^;;Mrd>J#wlk@P!=N0{w??xU2CE0vENtARnQY9G} z9|iG7^o5ge>l*E?UK1wG#tw)e(-ly+&@9d}*@Sir0N*)D&#x z5-`!+TD$$J_GD(RF1r82yep=K_W(uMm8$jiRMU&-M%?6qM;mwd)bF$965Qu;A2jsD zEv*kut$-a9bPGJXfLSpCl(QZ~2j`?@NI*4~gn>KU=zf?{Xb`(RAS{KY&DjjDhkrq3)ylNiggD}SC634feSo4N zt)HonOZgAIGp`q>5FMIFR>b|y&b?AB&@_Jj??m>HQuEA$-wxCUbp?`gBFWmxl&O>z z)JZBQHDw8VZT<0w{JDJ%yeK$UJCn%~ygZWHFNz^}?S5EXUT5Qi59NyGo0P>B7vaOn zIqKXkqIGFW`mK^f$SxCMPr@L-@h%sNEZ3@gJlBpLe4?#A3AB8;UnwYZLWZhS)_r*0 zfA&8p(C%2^l^N)UF}v=P#43b7StS9F`j24kQE2*Q|DtFdOe=V>==yoW-)ndK^GIH! z1yck@%aN%@nRXEkZY2@=g&) zZFGlyupHHFoxw2luT)q82jY#hppir~FVD%@d>?31t>HJGVtSD>k+>hBNuP`cVVykL ztWPUw3E<5U&MOhmr{fRcZkqYGA<8Q_w5v^%E9lrE=hZj$I5vMCy~~f!-!f24w~_ac z$lcw@?sGzK(2s2PhnMwF)gwL9U25SUYq<3G)qS__$w2MVhKeiA8c)j3HUGE9&C zI{xeJZ@==m3m5?D%>f?1zl-#w|Ep=5Ezcu2K#$s`CODr0OmjYY5~!gt1Wbush75ua zqP3^K!&*-@0)D%U`3&?<9=s_Sx6&JwyT!BJeb?>R)dR%d?;B_kh(t)qqKC*je}ij) zngL@H!eDD*qRSfmc^?9^ZLacTg_vZCxzTQ#XP2ZqE4-gU!D69~vN5*H)MFS!%7!}A zOb*LQH6_YwD4Ri0!KV`)Vb17kHu3+Ml% zKb;+24-X`O8GOVyl8-DWu)iEBnx7y}zG!7PknKX{)j3#6!R`MOxl=g*fi3Z!>27M@ zqZ0(PFf%_pU&vgG^fn|OqTiWbyKJM86Wi;c(B4bqPIYu(Ons!79p;(a+(ja`iRnSe^mQrq6U67jN^jlw$Xvr%nlmv2;)28L(e15g z+eFRpRiNIIziDN!M7q|xyyt4mtM<3$y9!JRjP5z}CD)J0HN zQIQ!p6idY9^G{&cWGDkh0NN0|c2dOMys#**MqF!47yE4kIbCXjeLNg7#_$1s4WzP( z%rYb6eo1CCju-VRF~Kxs5`d?9hSkDmYwTz`wmsvQQTA~z*9<%)m0AMU@iyg5YfH<1 z>FC{eeW>11jePrHW_dH{DcndKo(;Jnd5 zQya;esWlAN6r9KkM)^jwjx$L!nRZ#mkhm)$^DEIX7<~Uvw_lluld3Agyp{UpXhSyg zY+{Q|M|f2HRE9U>wTjRrd-;p?d)8RD)Q&Ufc9U)PT&NBci&@FxiBce@avwY^A3c)H z*-}h%)(>i})r)n}Mv{zs6KOTZ@+78Ep6~eCPBO$(rARYrY(Hryj@Dts4wVzeBO1Ue z4p=R`O3n_^d9279sS$l%CCC53Sw(|-T{`|kUe1^ir_W$8w;T1p`popQeW zAlGb3rmGj8c68-^j7v^d$*V{bP?^HgD2|)%H1$S{lEZz-e5_(CB}q)u=G%&^va>ze zhWxRMC@)!%s!`|N2+Y+AEWS)AcOj9f9coIesF639+LGn|Np3-R*o73!2tx@Wg1)+a z#~0l1P8fw@;W{|hPSLomDB~tajFYqm5V|9pz4KGt%sj`1K3;P878`Pt5R-Zn5u@fH zJYwapGtxSTJp7$=Yfu5#ZWk@!PfM}th}*Re?zRw}#d;n)xg-#v-iy2WPFV5O++GlWJH9r_eeqr*?W&7n-GwK|Ic+vS683+>$%5sKlgJ#1`gYDy_0r z6!CSfHX;V(p749uTh%|m--eMPCQVanmBL4vlj0MMyYJJLHPzxvWq!2*-zw}t!w zHy@G>#FUClOf_$w8YH!+!ts5zTPw=qd-K7?(X~oAa&da>H=lZ^^29Ia`iT%P-$v0O>q$CeoK!y;P+gKEiNx91>T7hbebT>bh z;cdu>q!IhW8WWQ2WM!*Y@{6AWDn^S5-+o2Eib>F!q7Hkno1uV-w+tzNOfc`*l+&yd2rB5;P$>ZX=%e61_htdRQE0o!=chOT>t`k^Q`(Br?_AMzN zpzie!9eAxEUWPkqQOK>VQ-f0(N-pb0v-FX&L!3JgOqV3ET!Rf(FJ!Of@Pro!vs>WZ zrjOnCaPy{h{Hh@E>IR1ne;7sC&efDAWucmw8kSid!t`<4#<6-jp?30~D^~&@%v9Cf zxKTO8&DeLoIE*u4&xKUBLed|{$1nc)uY%1*45+Q<`@uD zT~dp=m<@HpkQscqS3+EBa4#6m0hZJFl8Z4W7V5dkpip=DE(+>P zYn3GqT0A`T%;e;HpMx2D(28N!O0X)aVUkc_+~}`t*ZI{k3-*c~;`NH^WqgFs{!#3SK{Ab6tj&MN8KImrN=5cgs$=m>PKZWCc^I$Zav_xBE> zn57`XBgLb9${^ggK(kIJb-kf@EaeHEf)Bz6WtR@YDJqJ74amZnuYA=JbN78V|yAI%e^oY6z9mN8R3Jy@Rv4dD)MVQmLu<+&hT!RTd@0 z#K_&mC;>~1M_a|XOHy$AgNzYOTW)qEht6{Q?DG3@JDx>$1BVlFJNx)+Tl{{$iKGjj z+{kOYECgA8Ax&4%X9}hqnd#ymj7hU&<$fz&=No%SksljzgXTS*c5m4b^1=J-qCOP~ z9YQ6gf~}ZOTa?#Z;SR%n4<#e~ojy1Xx|+LH zXJ!hg;##~wfsB{l)f+=(=u>&E5+~Q0xpjAN!jj)P!P3kekur*V&W{>L9-TUIjV!<) znN^qpf20jQ3YkTTTF^amdz#9^I81VF_i&=YF&H%}@LR7B{}k^c|Dur7!p^r)7!nb% za;!ztfL4H>hQ{P*f#DF;{!*R1pv8o5Y<_kf8*bzM%>q;-rV+&chzRV!Ej~W@*Vv+n zT<@0#p2|F7GpUM;nzXDU=zQwd#E&2zFYHlmVlL45#g%ZmV42-e@7D9h>qYYQbz@+> zR(Mio)UOCzI3be&hdU8xLtVOyBwsO#FYT0_H!g!SLdG+CPoevAIU5`VwUvZ}v10K_ z@jl0P^f@{K;uaDC zijpVNkCV^|mb)?K8XP3|P4?u=_C^fK=w$Zb< z!Fn;4W<>S!rHOpBJ^GPrN$C8`Y5^C{n@2P8zfdmH{;J`ijY}}bRx&Do%h{;TNEyLy zk|OSd`v}>J<<@N^KI<&0Z#n7wl}h8V+BaT~2&$@IG~DjkZHlSlw_Y~THnuUA>)rXn z6dXa~JFj9Z4?W?&RM?aQzZ={fCK$uhA0n>pDw_7M$sNRnSN#N)_x;* z5lk}`tnyDYLnmy>T_-stT$C9=9M}--Wib|Xw5UfOsue#MZ@FTkvx~x1c^6-Brg%}+ zd6s?bASuU?HtveJ6}ap2rq$5RSB+YCRN57Nc`-bn-mi>albA0NchHhqo?9jh<_#!4 zhw?d`JhmE3lIoEM;bF}Ooh!xpn>7Wzcyk)zap{E`Ep_1znuDR&IyNs+>Aum+d>fDv z?NL;vC0!r?(7%{xH&JKm>ZF}E;uLG*r`&z`xLNvd-=a{2QsIRtA`C6&G_UA4y6&}H z+T}lgcwJyPCguZe_M}&H&kf&X`Y#&-{NW)Cg4W&&7R+muU`e;Vz1m$?e8*VFHz!Ye z69HZ)+;v&aDJ8EYL)K;p(eO6BuL>`*RArCWvM1g`RF%_1=h0zL3XRXZCVbXcvvoP=``x2X9?RJ-M=dxB~i!h>M5-6TGs!5wa zpLCt=F^*=q4=a^VOu0{xYcl0zy+G|C5_un^>{C4p3tYi6RAS;QcPgK7rx8^*Qy#ph zzi&LForacfj8UZ5I=~yKZ<+RKbs^ZAK|%gW#wVs&LtzO+^#y$D#VfbX z+g$S=C)M@%qa?sMdgfmn?qH_AbcK@es=D}9O%CH!3(<5fy`j=XGy{beoH&a~Th-L5 z2eBztFW&@HNhDGscH;=%=4Cc{(}BY@gC|97R^r>#Ux;|{-5xOe7y*biSfhFf_9`iy z%?R>;CX(PrA0luYTLAHEi1rwwZcg(+;mD;+OB!Q3y4d#+J7(c7p`q54J$uHz8Vf;M zM20+$aN+yUywB+cI-<^BwRJVD1G0F3A zdvnncCaKk$C~(b|B9WCc@N1g8P{0h2T3Az5vqe%1D{q*T15-dS&n$khFyeaaUyG&GD zno^DiK3t#@D?Cq^jY!c6S5@)IYg$~{YQxt`@r^4zNBpK)o}87j-^LSQu&R*6rjsb1nrt)||8X^zB-)7elL^ITwX z>t#Y_dH_3lM{|@c*EHe03X&6L&UEQ3`xZ{WJFIYBZF>wn1H-qk!$wnezxar%bP@i- zGVv_cR4m@RQr)!P2&2)(*O-ZqD+?qJs)nvjZ=xj0JYV@le%sf!jaSgzqnx<~qprYn z;I`tV{Ioie(lqTDjzudgS0nS}LmS1p#B zP0!jkCxVmnM~R>azEq_9ZDD8z_iYT-{7y>2l|{2McBN%qo{O?++ItA0;;@q?e^w*uR=03 zTX`QCc!dPrvI>qu;;$9kJeLz%xb?{5wXEA}@MOTPi2N`!oc1dNgo?8ik41Fv;9h;w zdOj&r*xP(b)D1uCme+x13Pqw4i{gHwcSzM#fAq&FS@jkb&x*&NrB^68GJQnlKYCV> zQgBUFPgLM~G_0_yEYq~1e%^5+y{z4{Upe0kJM3gJyEHI07w(ev4wKI2>L~8yo_}to zoX+EoFXokDy;hWZs8l&ka)VhpDxyj@gqF(>P_qn4DG+Cic8s-^H*9MndDO*8#HbdD z6)_IJ;c+6$1w2)3Z^fTQ54_gL3`_Fl-U~Dw9CfU_yQN4)FVWm;(cE}5h8ab?JyF{< zImx<*sc(gnJ$dh~hw_SzT5sd?=^#M#h7g-6oXAH=rH zk;n}sD%BL$g(w7guxD(9&K+XT%=n^wDq(^HuR|WLA(~NEVE;hYHmv?ez6^8wz|V_e zpo0a4H%ZOp`=YD|TO8XP@a6-q3%^w|{696azm{BAG>M0C4~akCdm)bm$Bm!h*RhHM5B4i1Xw#URlkzPJ4K_l*L2D1ww%^0$Vw%FAzB-_GKy4cA~%_j0k+)GvbU z_V?}#AHG_cWN%zx>$1`G#d7OSxaN8I<1 zq1Zso*1^hPf*0|uGNxId?PCjzyE9g(Cxq>#Qo}x5-ufMX!;YL+@OulIj3lP(*rB^x z0$sD`FGZGabB0b`Z9sM1WeRQ8+KX_(lG}b6hD2QgAGx`NMYqR#kH%CWI9r9MJ+GBY zfGIs8_mJ+MP^U#R`vd>A0{=xy0hSp=R2Eqts{8LXTs-Z2y3lB^W#zun4|va$NKukH z#n##wiDQ5xjbz3dVYZUzp!JM+f-TVV3tEtYZyqyjy6m^ha!m_yysOPY!7(m z$XCViDcF7DTO+4$&r*I2XC8vXDl5iyF=s(4a`|TYhwjRqhpaSYpQ^9r_n(8vHW(sO zj;5?^7Fu4Jzonr1>9F&r^y85LI)V?YEH6Hrc`!$R5=grWzrx0|Bvp(0>d7ISJ zgvj?OQ8T^@4#}6{LZW9qo1=c5dDX#ZPlTR$wst^ zE-Z)EbWm2a$MoK1!i>2m z`E-?$gV_leK2kR8s4+H=m9}*#NybC)b+|yGl*3ldWsJ~tbhvE9nr*6MbpTGk`pbgM zpx48hQkf!nRh{`UY#nYIv=>#DAKEe|eHDJl(bHxHS3Mf-j>tVrjYxri{c6Xo z+1s>f4U(4;;}=c8Sz#vLee4(3`mOQe3VI)#O~l6mMLRD&3jPamce&T7nO35V>iDc) z=vrt;A}l;LvMwUe(Z#@-RIx`YdyYt8tqZsM6@Bi0rjsE(2Ze77l^+S8z=gc0tto3+ zmIe}%53&}xwDg>KCG0lu)`)$$@xe-d%Zcuw{`Ey-gPZ#K4H(n!z0OT^pzmnn?ngY? z{8|K&sZgax@9mGY6bZ{~+dXipngo|JH|&da;xaWAsdKs1l4E!%v=AQ=)w z8o9oyF`t#pB0luYFF^XGT5>|aJYgAUVL=^(oMXwG@pzk%W>+TV%_Yt3=hAZqO=*?4 zS9s6s3FCbvmY!zQi(pt{see(4rYZI$`G7dqhXL&3Zl#JkbtrzA8bZ4Dg^bcF{4l{CDj6r@{hXS>FlNIDjc>PL3-4}0;+spCTT}0M zi!{g&JPX-p;T6K-jl@vCKPM;+PlKU;4?%tAmQSvSGd$9=6>D>RB)6<5T`+Pp4Iv`_VF#tN(C3R0ad*ZD@A0i0rbaspB{IW4XW}bVJ@muX&l1fs zF}nTX$()<_6dYkrnt^a&5o;vr^MiA_pT7*gzLYnJSIQO{x)fFWP@F`;S1;rf>0XmA ztx^)*Y~OiD+H4ZGITlBPsRL2W$n(g|!u)h(#5-mo!y7UyLf1DrG}R$km!CzgQg=haD^s9qd6HTxX&>d)r#U>rP7(oDL4@Y)6xn#Dc9kISYSd z4M!R#rk>D?l*n7?LSGzWngsk9p5AjND4n{i|9MY_O(OXzl25Mm{&!1EDfHK*sljTS zBRJ8j|J$NOTM{^JM{JLJX<#<;zYUcKI*On2utKM>c`xn3CR4}OYE0kKH2O5aBm+h4Qj z!rod{V|Csc%MnpG?U)E*q@nttOSkdVp0B{Csjb(!y^m1KiKXl^Q<$lRAwjwx3mznw z)?-u?T>sxtg~;OnxINljw1>2Cb5BG7)xaa!$uhhkNwsD3ZHGsDU`XYK7OV#k6_dV% zpm$+;yYQJvOO`vT=)b>V_URJ^O{&c0^MtIf_|%8<0W)R~9>i0nQ|T3#uzgWZ|BB_; zpJ7FAjWg#jVo6kY_a$~NdDd(6E1oTc>t7tnPGXrU^78h15 z8rIi}_2+mPm9mSjtwtEFvB{aah0CnG>(8(;&bArg;8fJhSE39hm0iH8kd{DGq2I#F z844WIP|(c{AIkF)p7_+mSZx)3fE`NqAt}M>)3=^)J-lDL)>aM=X5(QRQ7vf6TkF5oIr6N;oCPjL27K<_sPSxdoUj_lzDdF;#}y5yoSk%;G|M6VHwBsIHZ zU$`pg!JSLen|m)~j2ky2(4SNT`+0{KmWoJT_P#q`(1)TH4Yy?&Hd!-xp5lxTtzo2i zdk?qf2z)onj2_)`Y%3Ww>mXd^yFyll$mNn%;4E3eCU%R>${o+!Y5CoQvEZCLGzlS( zFDwNU23t2hlzsCoitZZwc_pcq&)YASW0QmB-!E=SbD2<6tjFT^j+Glhu5>UGFLS%w zT_~UGyU=f%NRqIIZ#+INT0R1FHxsb5x6H^{aNWNC#opR#ejE zbcoSheDoCI-^H@Fg*>O*6~0Ru-Bon=ohbbyBa&x&mn>dbnZg!l>e98Wbn4iSTE?+9 zw#YvW2@;mF_%tzmB~pau(ZIXPOqxI!jV6tIy&1*?<7F*~c|nFbe)*aAO1`ZaE1KjM z5OYT)F^~&12tX422BnxTqeTWEM}IuXwrC2@!`gNw9OU5T3@@J^~8YDbtVkE zX$Ooca1Btc9|Pe#`|H!bck@LUl9M%fIdN_e1{5-AN(dIyzpb#BcyjA@IbW%HFJXBA z$>*Eymq%ZhE9LGD@w?;*sTG$^LHLwV(YqUcG9h zN_XAtQY3S4Sdm%t2tUkFu<5x%U+WHu(g6F#8RzHo>WRo^!VVK5m)kRyy|L_W?u2POCixAA15cQCM)VzhX6jH9H zs#>@$;-?yuAk7ia$_f_OvS-C#rNcz>NBvcCHRvwt`-qQM4L|C$(?X5MBj>T}w9fXj zQh%mGu(`TAV;YUYWotU3=w(iCb$&QoNommF3G3$>f`gf-A2I3Ua+!y=d^DfNOzC-U zdwhivt+8!3@F(x9|0EPF8JiE=v#a9Hv-Z^?GgwJXFD7<)#*K{7`(7!et>5i>DUR(n zfk0@)2X6xgOT=|Hfmz(xP`OG?y%zJ>WUFv^9uyw8j&oAf5V@Y#-M4e;t5}heQy5eJ zi_|r8Yu&F#ORC(@9f-|PA-tPe*&^yFMq0G>DY3!Kthio|5WcH(?Ov-+uWWvW(esgN z(YJ)wjhLl*sZ*}EEL$leK2MVnWfzD(q8RUAl#W-z7uiUlc$}YuW0xwa*UGg{v5Gw< z_HtYBBc70?_}hAir_*Z%Qm8&Sh2jjU#rsYC^Y)*59q>Mni((Lj;>fbNAie3%O^Rd7 zexxDJAuy&I>zj|l@LnQg#!62=scv7y&Y3S*-91!=*#V zia>&RfSFD)ijJK04yI!Ro#E03?9Nd15D{`>HPhpX#-%Pue0aP14G(=+-D_AMwL~G$ zxCO@E`&5kS?zC{KU3@#Vqlp(%%`&Z=s9Tvmmd}ms9g=6yiT5#C%EhYW{((-`ZIay( zt9e2VWQ&^|p2R{Gn&&Pt;*BmUkLXBh>z1p~Y7{PB+tzA`MpOxBQ>EzYaV2S4$ru#B zD@!ti7Z;89Z9R3~Wv?&FHiX)vg#2-n*_%i-08@3KhwGcM zve@&*%XyFH>cdlmM{Y8egz1zIj686`_;xeuHkr9$v7naImt7~K8i8(?dpCya#_kut zqA+BETYCpTZElV^EpLk!crlmC+RCuS70Hn_kd42p){%zAMXs`LW3WcMq3ChJ<~kSL zhEBf#=7JXjz8R@vU+6s9b*_C2R~(%Nw`Od?PUEoa2A^{dR|412WDk*{w@&e1Y*p+5 zdBOzL}|HF$;LNgU|?nP zjb10$Q+A^F^oZj4=&T&}zNFzDn^WjkU@qXw;A+(<3{07Oa4Xdy2k(7h%3OF|>-){_ zE=+i9LY7;vxTkt|uYH=@tex6;_I1+hT)nc_wR>D$qtmb!j#~G{3@{OSmr(WO$!Aa* zvv?TWUrEp4Fn9VH!*$XPHOBf;xoKq7(`3{z#b+WUUHo#xfVpGCn)4o~p3Jlk+++D^ zRk)7rrtSN8+E9lubj;fO<;^7-;II;?{RoY)Da@-qQke4zXvLR{N#Tc|lLP$&2XOAT=L*sLa zAj}dpFfi!*U}=+@BWF@rqmk5aEmpLz(5%fSyC<*Dxe0f(hr`9QBRc{-x zZ{B*EgIBIU?r{Buaz1XT#tn$VlSE0ry-vH#S4|C6*FN?24rLL*p-wQmyDd@W(#9cN zK?U{((`8*Vy-V)844jjJ4 zs7c?RtVAroRa7o5hz_PpzLsjCx2Yql3gPEI|C+AvsmIg@thqRL`O=G`S1`uuY@D#m zuKT!AM{p=-wpp!I8Z&k2;3;3wXYw9KabEZ;Ha9Z1{_!BwCL8eu`6w}Y%=IpP%6A{h zJT7wx@O zJp8AwCAehljN3k8X8+1}5t7m|Zi^W5o_uQVF-3sk^;azVeV0C^VG)YK@7PC|5HA&@ z8D6djZwIzU-YZ7guxdDbC%C3G(YdpNyK-*0;E+kgTWWi13fX(wV8pOk%P_}mpA=<7 zs^M^1aE%lb=g|K#$g$J8RWG|m9uUbI+T5?r&KGkJunF9j!$oN`SQZPru9K32JE>7=$uuqa$P8!&?oT^$sr3L3BMSS2Z30@`r zl_l}Boj~y6ea27ZfrrK+aVZy+M^`a>Ii{=;Rq;*tOVww!1^mb>z68Q+)-JcY3!tz!HeXV{^eEjL0nHKhGbCLAM2|v3jd*XS%7SNgea#uQ0 zyoz8%Gpdj>^=hBuBdbaeRoT(r=j;r(L(UE4wQWn;eGBq5&+IHFd-qY?R`@~e4Bw?M z?>XqO`jI47rIIbA(xubY1%mw2;0e~TZ#BctH;KrHiak6ZIemAjK_co^G|yf2{&0r0 z)~)h*lC9GI1?gwBQVHoK@4^g_zUg9YR1nR7dLgT}h9Tr5=6zr=K^ReqF6A{yRq?fy z#4y#6`J9Pugd5Kij*>u6*_LsIJI-!1E1w$?R;VDKJ5H;F&`y&o4;9kGmTa-y%V;Xx zA*@qvjCC7~)Zu#{M~p9srFK4@-f*8b+IBS6%?!~S?z{l|GV)veM95T*!AE081QonH zNvOg*d(;DK2R33!7&9CQJm&C9J64uwfdQE2B~=a!~Ntp`H%RGzpxwr_YbZft@>b5pqx1AKNKemdXC<6Pc-FduMSszLb2Ap4V zvU7il=~D{+=w^F`cXGcc-L6h@nUb{N_We55vA4z9nG#}~(=53C4WI6woxW^llt@? zOX_ont7McY(rwa=41JcU45nrL(MXc)9jlC@O%V#Vi_XT2*uhxVt1jr}=Ud>!9;iz~ zvS;`1%jOwpM|aqL>-Q0BTSEt;Y{5yb&yCjLr6yHV4G_pBs6*e{7 z`8;-_X}Zazt+_|uUY(J4yX(Y4wKQZxxexOSwiwJ*I6v1$aKVaZ->K0MQ_?B5-Rjhl zU7xHh_wo94VNMw~&8$fYjfY4bCpU=A=;2@#>KnpH8VU->z4;6you~^NbiC1}YD_rX zpWQn+a3@06Anno>qPJsiRO9lp-pGr?q1v~Ftu<5EEW8F6F;qbBXd`6vVpy6MZbVXD zKRAd>L2~Jh3}NsVgCG^4i-oM8)9qUo>{VP`<(G zRY$T=b|)_fBeQ)+D?_Z};-I{Oj_hpxawVJPc~uV2?%1R*f-AED2p8>5iZqD(vrw;7 z)ro#oaeQT$Fc@KH8m;ks$l2Q<`_?dOW+cye_b4*Xe2nD!LLbMC48{=CkO0J09oABq zV&)a352Nt|$u%*~kPsF2R7WX<)#6-z0s>|vf_Aw)ld1xYHgSKM7Pz>6lY~4$>L)>K z&jfgMbja}qJWAnT&QX6;f^WXL@g$Avi&6#OND}PWmxsp09A)D9Q3Fi;OTJwp3B@lQCwQjnb#bcA9YuiDw;rzk+Wz0QY)ZRHnA%oa8oE|xM zfkZ``>)`-LGz%tcz=T0rlJgusB_s0sWd4HV6#Tyb?M3p^< zG!c)24VfOODM7x(Rm@kn1zZo8j=YVVW1b}HZhXpI{=PD6n`%q>rdYaaJLW6fJ>*#VFPgQIqCK|qwZiH{(IKCRV{5i&`JPrzRzj$j zt_;cqb>7nJ=b(Cu(3yQXj!$+$uuGGU+HHN;Q@7OXr(QM-`W%%qg|(+LyXHzxQ)6#!SdhOcY^7OH7}z$8 z_e23RO8tVq4vMSW2EX8wrQk4=8M;UH==%B$UWbgHBOex(?tDxsqMJ}W@8KAYe$FE2 z#jZ~EZkE$c#`P7Npod+=MQSn3svQ$w^U&hd-MZB8m~e?-y2DaI}`7KoDQT z{gCSBELc~bLmD8x<;@>1AaA(O@s3 z!1ZrpZd}t;UiHA^|Hfy|>6%JV;`tU$-6xuf;PPP_uVD0qi(kL4izd0jJvYo+g6;Qs zX2p$DD%^p>gtZS}&qf58+4^Yk`c1p|FOv1wP_yo8Q}=Lttl)LjZ2Iel$XaJ)d((Mz zeC-RnR({a|3p0wb!B&?my)!OC$OT3DE7p8(EA7VK`OFld)PYS@u3HCT^u2~9?VEI0 zQhcWGFFr1RtbXwgy-qctls#jm^-y8zfbre?k-o6!y6KiB0rxmW6^t9W9El$4I)Z%Y033PmHH7OuTFMypZ=*Q?{Mtgn%-;wu1gco|+182A~ zYqq5taKGjlyUf!GMI2eF(ZL3}kJPulf|{-2quIN|H(H zy_j|rH)0Mog1NWfZql|T(rvQnb#yn}Y2zGKR-1j`hEVjRvdAAvc7WCnjVUWNtl@FX z7rok~W-3^nqSdvra@$@O1Bs#jB#s6JqiD#?H%14@tkl~1x(rqVMj_W=D?-Y+BuH@NLny60@Zz)qS} ztO@GYBd>}*it}4DL22gtMDF@YICHyG$VsYACV%h>l6ER8s z%3y{?dzRpa?xC(3!R(hcP40BPZ4_|TP7KhqkM1) z>~3>CcNRybpw_Ye+z;|xqucSa1onYsTupvA-$5`d_GKx5VN;`0@;wA;>!5jjhIJ+5 zcD9h0s|%y~HXlj~^9kC)yRQU=y4Q%C{QWRs{Jtj%cQgs1(K1Oa-XQ*v9%n zEdI1;4pKoFQ9mlAPQ8&bI>?{1tX_R73b_q9*u`l{QOTa1~(3+-Rv2 z`YKYRa0?r;Cilc$1G}}?LWeec`}5U}VzWV@GEuq&Bo&jjc=`m2=q$!w#|)&h{i5 zcaxk*6ydHM>TIIT-kuyZPLNT&Mq1rVHU8oKd+sC;dgf2rlc~w{Zkk_ma0n#nf~3?( zyYu3TQPGV8mzw%r%nJgRZ}lTPmS z;>WkPYe>aqcRoXu2IzX9w{_>qVUSmA^gPFSMkrskkeTryX}F}*YZ4oegXAEb1tlru z>zbh0fdoh3G?!Y9-#K(EmJB8}i%MyQQMrwX<@8y7J(YDjK~)C~OI(7kyoVQNtFA8I z+tzz{PMRIOIOxj6>#^DL5Juaa*GTG9(*bz2Gm!faWl-Fb-wvWiiH|G9d|+drRqf>%6v&OKoAErO+Tk|d7m4e9=LqF3YiI2a;3_jB~^GOwh&-08W_ z61{h)0Ee8QuziT4WXME3B;zwNnd~k3rG4c4(($2@*g|(U`b-Y*&*NWPt5@~udbW4jY2xuv4&c-z0RT2Y{0n1pD{INx}UN+ifLbfQA>yxf%FPXw7&7- zs{*W)FR!9%kz1qs<&oKDDbmv}-=3!;rBl_)48Gvo*6)REV4JNFP86Z(x6UoLp5Xcp zc25|)jyUA;N`MeSQ+4Y0%ESgKc&9o>bFW(?7UKF;;B;Vf&AC=Abs9p^qMZ0rDT?dDyu%0Nb-u|k;<1}d4zo!OyeM!w+G&11PgZ&9-PSlAD3r0Isf>} z|NcOQ0rBXM4>rsPk)yvTNT`W0$tX&)9CUzt82|n%3XC)O>awHwgaPk(5wXV^)y-=Ls->JmIfz9R2e}2#}B^;&ZhqFbM(EV^U+77Kfh;V$kphF52J*7Snhv63|zKn{lh1+Ks|BgC!c6yXKUi( z=m<6^{_x>kP!Hz;TRHw^g1>e6hwOhe_y0WH$l1=u(&UE>ZYW0B=^1~$%;`ry_>V&# z`O?hkA%7lu{s-}A*Yd7NM05ZuzY6Zf75G~Y7?=!S4)8BOqbm_(Cvb+^#97SF+8Sc= z&z)G`lT@}6n(YAzA3XOTs~nLIL61BQN#4@Q`OnPbdj>YHQ#uh)4H7sG_K#JL7>!^b z>}ePZM)rTbMe%zGYnIQB1%Sx?3&H{3N_1Mt|6bbnJ>=fI*KD#tQ#4@RN(9O->mZFg zEkxDQ#@-qt0S>!+9Iv>fES8V10H+CXvQW50tY5{+*x0x@8yWwQ`?hWxmmlad1GvCH z9)4sncd%*slwuw)TE=wC%yl4|1n>Z8pFp~FI>aAuBsd;Y#is=80-ReIfP|s+mrrmy zh$_Uv1q3xqBkPmh{3TXhq%ELb1T<)^d0##)?S!60T~fG)A3KgjEk}F3kL(^^smmd1`bO7%+voF7XE%M?e~_sH;}6s2Q2dtX=M_#fS6c8O#cy+QpWiN4_ve1DH#HT z20!Na9!Q{h2H@i@ID}KOe)OKnbHMFoprlm+n_7QS+N01!dGyDLvSwA@qoH%(?svGyOF7v!D0=`p4L0aB-_hKDnFjQF zJWXC-&+!Ft+B-m}l2Ec~-}-eLm?uF@fl-fVioT6`n*f+s|HX8@{hQ1a5wR zN(1}{dQ5V>0i3=K zq}|XHB3IYtbbA%jOn^NSj{9O*>aiV47vEo4?7zT#wUrCH55bjT&vfE9<5v*GP;VNW#VYkB331wecPh|uHg=gD6s0>}BWn9&lp z@5TYJ5;&_Ul$e4uzX3f_$axB6i6h=T7}ueB9`AmY_ak@BQS!-L1WL^UOla>ZTlrO< zhLN=k&E}Fz9oEeh)qHdv5&#>_i9Y3+lVd1>(y)kl{k7GD+K~V=6+&d-de>EK$CGc)7i2cds<!Y2BP$(t1Q=?Awk{)95#=C$5F4lulfueMR z6|B>^{@>}X#Fn$~TpXQx$!%Djay5@6G`Ih^eeKd_p$2tr8iDI*oi7mT>6i1!r`>ZP9(rMFpd@9cImKh#3mp>9?@LvPOG?)batj<8| zDgSGLSPz`pWd}V?=U-7%P3d$bkWK%1EUIyR-`@hJy3AnK1Ff@JlT8 zc%f@`dh*dCfauX0F<8_1k9lOt<@=i`=RtLj^80KW4=nKhjGO#lk^cogUeEkSy9<+` zKj#98MWOWU?|W+KUxNOi?lTKNSYZr!-+-4w^BVkqnfHUK?acb%&Jj-;@Sv^ffA91> zM>{Kst*DWsqb0=gKN1-EIGXSxpinSJ_%4A_3OqGf%*fjMsGxGZ4a2YUS(*SDb%2b} zIht0`FQ9BqAb@t168?vXX>AQ-U;u|H23$!f-=PROJr|T$tszI$ANEc@=^6B+tqAL2 zs&aHI?O&e^-&l|tpR$vP{k_>m+Q`Y`sDAMiiInoS;)F{eny-OquJjik{Lb*b5^+{7 zIB}%vda-(A1gLx*Gypo4%8WV#_>mfZFnx>O5?>+%fg%BfMpY=$mt)R|{^LO>?0_X` z35dT08UdX|-+Fik4fxMoa@E^K%n*2ADDXb$Bzhq6Z1^Wy{ze>g95EPgF)qQtfHdXr zPlj(&@)@92A?7v^+Y{D}&Y4|@T>)xe18RrvbW~uc+HZHdpU4Cm7JTx#f#OmBrTDw~ zXVroe*6QDR7wby{G%rAF{jK(+{!R(jn0{OMKhCMCSvcCcnE$Bjxwg+eMg+-$9@Y1; zcDCgATXFojBHEwm2}=Gj^uYAcIhb6-Z!rH{xcM^~`rn%ezt6$S@`sn)K!}q8lT~PE zw`e>g@`)=fX~boOX29Mf|BCN&t!Knmv@<m<6aQSr^t}ae#}F1@f@v4;#qVh|Q-s3IqqE*?0LN+wgC2Ph1=* zD7|uz9k9%R!lCm+OEAv=u`_DXPo!7Na6u{UV4T4QV;yvkaRt;Je%T=Z zS1*6G;>3YA>yeR05->~vPy_T}r#E{>4Uh)6pvl;pLfnt<0~USP?dyRds6h0CPV#Qf zpHT~bKu<`ITGjzhi2&%(`RzlnEO90VI7;%47k#$imEAZ{{(ay98c@B$5F{XH0{_n> z?*p;IFcAg{wI_G)7wS)5%eNi5JjMsAPZ_2XVQiLXvGOLmZ**m zv^;7U7%b3TpzA@|q-TRaUimraFEm{TQ41IBmWSrPkURtKi4ikNI!GHC7`zJvEp@1V z=>W=pr&hPUhm!3dvoevR8tNY#%*2#5e_WX0Qgf0zI)QU^FFds0t*D(@6F_kIaVrom z1_%^`eXGp0-={7fT4&jcBb^-Y0_|>e>KJHL%R(eLcvz1}hgp%)&^lfSPTRGhh*A*SqjM#o;CsBV zqx>!Aav*?CVSYaogp2q+^zpo@2j1F_K+H>k32n{72dCzVL#&PdYv17W73o+k;INe7 zC=>K%m^)y@^eJ`w5Ab;D%x!H{ih;|3eInm&Qd!OTH5kP7c+#cZF{XOZEBHW>SP;rd zw=z#n0xJ*}Mz-cBO&7Fu^5$Lw_cR4|lY(NM&;JG1(dyuTFMD)5q!}auSPl4r-kQQ* z_>0h!4p%jIU%rM95{V2D+w`I2qc8ep)KMJ(YNdtpk6Ik45JyEvXunR-;11%n8h zp$9e}Lzm=UmYiKvj(4ed0wU3%?gt|U_|S2Wx%`*eKVD{%GARH{8ZbvYW`v;>uUh%b z#1l_yY2377dT44;#I#vJK+rMZ7yL!DInK*kgGt`RBL{N#(uo*zPPq#A06NP z3I<7N+IH=4&^V5#k!)EG`vBQEK@f+g#Wnl}jq`Y#tdeNFHlUIIOOsB`zd_?Vo^~L3 zx3u+0lR!4;${2myZ_vacW=1a7&R_`wRHY=riF0uM!to8Tc*$RHX#-e}Y3VzQMyQ%tfT7p=CtGu0Tm-=b z^xSZu7GtQ^EDrugYa}gg|0>agf&CQPLAubIz6rFW1GED@^PC?3jrM?(5U!S>9pFIb z4|gI+YmFh#09D)uX^jPxIz-=`DgMub_MaD3{IQ)>-pdWP)aH2IM7AK z+=(-3)lc-Dk{5>wM?;t`&<^zQi4HEEIAyZ;S4VN8RB^GjJzB^SF*UM3v8ejlvjYF< z$lo^M_Zb=g+^@5bj}f~b_Y5xsIjMoZp-gs+wf@LySfpy!5FjwdL8;;_e|PwBH3ef z=w$$~xftlR(9>Ur!!seDT#299#8RUNl@vHoNR@^Pr9`msrwtg#xZLrMj@Y9hbiAV% z;Ql7{hYSAih0WNe!K85mln zr-mL7)4>%z5{R34AU%QJj#&T- z#-~*C-@^WxO<%XOv;Ir$lhUJVjXDboP!mD~H7O$~zp?^XQ=K9IkLoJKRMXDU>W|QJ zeD@v{yj6=2RMCm`{gxgKjWcb|->Ul4I49?=L)%^WSzs>KO!fUvl5D-RY|-)hxS!y@ zYYv=oAGio~FRZpWJso0WWN%^T2%(fTvUEIpKM;7^<4N1-)LJJW&I3zC0mp!@3y0l2 z1M>0WUb#{*APS&WK;h7%%CN(4K>zhl%kQ`9Rq?;7%<~BTMHWM3sCYvCX5^ZSj9Kz$@_g8*p@~ z*MBsiaA41b6PR1_Uw-{LF_wB34G;shN{Ay!-<*Eb3WO)RU(bQ? zSp}w_x=?CApK%tg5Vv$PF>*9jvNnY{{ZJjuC0K3#Kph@H9njlY9%P+iLw=+St7qoX zLO>UKpu0m)%EEHaq6tR{)Jc;BFXB&8(O^Qh1-cvbn9ZGk2JGYe-0UOT;cy@@A{e)! ztBA}Ur-xeFK-6t5oligYzV+C{{?#lV&o|Y9ksu_k^6Yp*UV>IE(SS849Jn8w=r~0{W9!$l;|!Ytv-ls zN1+9JFdbR^C6t|u^M9O8{eP{UYe-Z<6vuDU<3kZh5+qraW*~`SD43eK$oeR2eN~7h zyLz?F?W*e|r4K1cCDErugGwZ%G#|Z;$TG8{Qpz5bVuDE$R`x`Y>_Pp<$I7|hyRthM zS@7HM%$fh3xifR-Oz_<=KX4u>yg?+RRdgypI*gnuRvtc4vs{OTsc2M5T6%k?WKTQd z_h;;CQS4KE#yHr61RE{oOhkR z&}2QCKevqWvA1PQnaa$fn#a;Nw6_WE-Ab5gzxbJU8vDt;{5q?#l?(Vj0>1T+4;*W> zX0emJcrimn`TaD}%l6-t;tW{#D!b!sqn{pQR=5Cn&@VYxm-$?(n`M=<0qZ0BudpcT zfxt!k!?=*o0Gkt(!RMD>3}D4B+>mT$ApKE^I}|+Q%?7rkhK;-;wJEtqoNv3*-mB~c%VL95G!VFU%dO>cFws*sh9@s&%5;KdmwL@JD^2h6^-BDXwU!tuI lvZw6j5kdYC?TjHA<(e8YtqHbSE(pKzS85f6krNnzq?{|LBd{)vLPq z>h9Iw*9y|0U}!)dI~!D&`7vK z;Wge47ItIkQ(WV?r&|? zypi;K{643WqTB{%dpsYldUj^WZCN||&8S(xF#y~bLo+JFk0?V`}^D&&?zC98f*qlil@RtjUNYA?%{`r^7L zy7xQ9EodnVxd(GT0;WYJWfgBwD~0ZmHr{@!bt~dJxO^`t^hAcMxdJ!#k@-oae*OWn zvZX^X-M6#~D8-bz2d$ZyoDZH3ocm}5$FqyRIDWfqFx!_|f;3-oQ$Sn{%LDJn?S@WAE4z_7Eq#X= z*+t++7sintWf=0>kK+eeKZ3^p<2hLc4(thw0(}<}s zdDq2fT6!;U6hy7DAxS%Adcb!}6PmXqU~LNC9-uySi;3xaNHsJe<&k*!!e5E z1}f9k`Hk|T91h)pijID3*^5i}1Zm@>^o>j%>m$LGR!m&=?gVfThu-~aE(sx=$hj1_ zh$liq&l#XJg`#>Zxh1qF2G0r`L|*{)!WJJ1Zaib7NG3k|0JI=MJ3zd6Iql+g?G_x*{{WzA9s{F_4P&>9~}7h&8SNQ?a@=*mqIQ~9>>gAo@=*`=fHo^rP;+QYE{~TNm@D~(5=9@ zalGOf$aB`0WWbh4i%IUsTE)b!3%r1dwWd-6RRa!oj8`F=mUzYw&rf=xw%S{BUcYQm zfa&kgLSWzi5G|ZX0YlV01vvHG2r$V|Tj9#x=h9@xh9WGaM|hhpuvNDMh`NCw@m+j0 z-6F2?6XgJQ58$EJ>5admJ} zDxlC7@cxbgdaI+M`xf?&y5fh%mz|H`W`cGuDR3cfI66`o4L!8E^rFZecf7kjMW{rD zb`Fl9$tXpp z;^L~gZZ^=2yfGgV;b~GyUT|XRxom@^ctelvcvGiDp4yiD#1CtOu^>lEa`xQ)eo&$h z$kY6kYycRD#Ua>_5^`Xk0+a36F>WANcGs|2<*Zuyg9f;_;cD#3^OF5U7{ zLZyDV#C|xN&8!CY3@Z{IzaNZ=-nFfzPG#s~J%HndImwwi_23OMko%E!CL0%=4bv!DDo)8KT#=~{>ni>*tE~jS9!a!*g<2T!pIk-plPKuV2bDF>(pz&ko3!aW5e(o_8z_aJv zp(F1iVo91jHePxNaw566cCQD%$=sEUztTUQMk-t!8)^RfHfB5oDrYMBr3}$!ihiiJ zKLh5}?bss$PU~cCo$W0NF<_i*Ch|3y*LSbP>U1r}yQ_%XbVLG#0Xt49=MWq@1B|x> zrndR2#yv)zP0vB8N5U=A@`G?%hqRf>I0#MD4nhm=u#&({8zokN<-e0tD&0%Z|$96?&8)9naoxM(U3@gv^Zd z@)MQzuF>`w9Ade@yq0NO;xm5v#=Hk&{odH{zxJ&!A7{@m?eL(D-aH4)5YoeDlI;TCwmQE@y6 zI(9}r624OV>+f5r{b;_C_{g6TZ2&I_>>?f0@L8dhKI1|zu7k^X7|guGMiu!OHC+-5 z(eL+RL|tC(txd)qy&0lc0>}oAlAIk-YWHUOTwQFi?6G9jiNqde#e&D=U13edxQsx# z95&j(#6d25is2v}Vo<{1SOuR5T^7Izz0YzDnhnt}#d$;S6#28y#N=v$XoCd~L0N1U znPxeROpz3CwXkbvraUms+?563kst{q3r&=*&$ z#@2hRh0$Kaa9}geEE(47z}yH=fBd+EIQaX;t|3j-;+jAUTKJ0$AUybPX7zlepRqco zcw?3()u^f7p?#%u+ZnW^7)e{`8E?_-qEcCznL`7p)e?L;QRO0c4i`3=X=t2n)tM@L zqwS8VGAcL+7R;#;Ipd0yOpc>1i@;l6J!F6mqKNyL z0YKDbsOB2h#n_yx(Oc`mEm2tcHboxaH%E0wsb-NG1`}Oixiz{Sq37o~Tr1V~wu0{( zmfbj$ZOh`eAvW>SCe%@Hxzw#FQiC^#{MIPpC3o<|+L zO@FPvV(sT^uxGl-aMf!_xXpMq_$RDmBkPe(UAvp!=Ee;8;o4AHwgth3Wk7e2Rj1c^ z`9Y18w-j^*N%sY6NMC5vMsIvU?8F~+&k+_)EtxaiL7O?qh5Ng4S(m-`;Z|rzGd_#jlj3(5fB@8@DqA;EGECejF+XA*ZYnpw` zV^w*UtvcEG>|Dk&jR(^cdggN6%|a_*2Y2_^xNq~(2|56Hb=t-iU3PbCdz4h;CD-I~ z&+CXsIn*^QpPDq?sVG^@(kdAs{cqJ3(f(zu9 zfAn|!;cmpB7&$y{vw?S}h6)EW{nemGqqrHB3Z@hH4-m@VS#{$WVe`7|VCfn+u?LhD zGF{{tHY|@}I;+)!DS=#z3NEFF!cINm&gZTL981>`^ywu*0qoYOE|_KuFVtwasp?SN zqdu<{;L|~``|q^)NUZEw{F%INLYq=zK?%GpeP=!oV$v%nEyM}V@7BmT`){?#5~$IM z-GeUk^io0(tkmQEy)(h}=JRX^EEGH^+-oJV&E+cae_(2X%ye?FbEG#=P{5t=tJCK_ z8`-x~U&jCj%q~Z(Rd+~DoH@Am2)Q;zp!l;OVtsNfT z_n$BbG{5fQ+v7{X{mUS~--N!-3BJTCE)R53KDK`D1@`xd0dgq7v}}p2=^FQh)2-0T zlWLo+Xejdq1R7XQ?Qc?*1Wl&>UR^N);wVtN*YSmzkSpG57%z%W_V>qKeuiJensDI+ ze_tsnw8+_3fO-KZ0HN`p?{x%K*_gZkPN%&B^7z7H6e=$_2M3}wFg&;kjLds-p7-B> zpc{b~!Dd#o>j*b{D!>Y>T#AvZt(t|moE_Zj$EFewvL4*UUXDDQD*gEiz7*WMaLc!? z&9K=qd@Fgh7^M$3B@R5b2YV)=sh3R|!bLKuFn zqvsb#BE%mk(0eoyHv<~8l4CCnjbkh#8cC|OV$aS>pOloK3uVF;?C`xwTY4SPQM`$}$x?dL7wKTtZ%Uzz|2$kV}{`@bIk?*;5HO1Cq0Vf-)JUX>4v|Y4pFs15tu2ivIftHDICP7 zLMWuv!o-qBx)dUe*f&~uG0klEaIKujRAuG`U`HuPp|0Cj69pgIZ6;1&x51@{v<*qR zSIaVZqO+5-f=zQKEb$GwT^jhBeJ)S)LiUM;d~1_q0^7r7B;FoSdw?6-{IHrC!3T#E zZa6wf?cypl;E7r~S6yT;n=KfZ*v;aIp-+7l7~%Y)C5b8fAU2OBEMwO$5^5sNq`a`W zHYhiE1cw+w8gyn;xjwUKJ@cl7hHh>a{&S9h}%m#F7tLY7!JOh=V2G*?F;Z48I8t#da5{ZZjL z=_0E5&Mjaa)iOHHSRk71PO$ATTvWXa@VlE)3Og>W04jmZYD7Q)`T9z6LGTW=eOr(+ zGd>hmR&pSMqIgeaVcy}qI&W6267e(HG^5CL-F02O+80hcjYdN206kg*6}_nHOCv<@ zp+!Bu`RxE*<7a$vXxQJ9PvE(SGHm%%gtmrtUHec{?GbS{GoQbhYUcWsh@Q##GvW}K zf)MfYAnc^IVvli>A;HNckbqk2SL1wR73d`Y_Q<1otERlQsGyZ)N^)Pd6*Y?nQ9KT? zyb*E>bO}v`bl;NRLG}~G!aBd~&{m&8b*pt_C00p=aPiN`;sJ#zh%$(-fF}g#KRcn} zIBFGqKh!L*iOZAHZH6nCA1+=H|Cw4;pu$!S{#&&2{|fQHOkV{jQxi*LLl@Kkkh|QE z_A7i45D>f&a$*o?or@@+`>dab580DspNAiXDxdy^i^-FRgNZ6`5Mp8w0ab$yBA?lV zoi4uLMOBN3C~gpDVi0_-B)suFq@dzfnnrTe@iU`D6?;K7BU&tx2?4a6J&jacOY|(1 zDKQBF5dk50+4)Bh4?rGgnAZ){PE`fS0ic5RMF8bi)W=PHgjT#?UJ^vY!%-4 zrI1U%AP~@%5I=!G70Zl412E>Q8V@o~IP7!jzq?;vVS6c-p_(Lc^snFd`V<~NnB-Ce0$kH^BOFJdY-WN8V=Q> z$%=+m!=9nw{>HIU9!$s`$)&gv>*P!cL_8*%PHV;-yJDBL;@w%a_5RP7FG@|(zpqEx zDo@J3E~;6P8TQ&xo1L8PPC57-eXZ-sTa z+U&Kr<3(rMHNOgC_6*YLN%64?4^oX^Ik2=Af@q-!>lkC9_Ti9w>^$L0@4d-WNcHC& zun>wN@420S4GheuCNxhr+Ir}1v(e}b&W_E_1y_>7OF~qiM+VjpV^5a|`-tjm$kAgW z6!g}mc?41*Z5+s^FU$WsDpR$wlkb5d8a-av3bbTRTfO$Rdamv|k z2`i}YA_?=KVaJm@Xi>Q0nU@g>LmjAE(FFVsxaSH@EmZ~-QH!wj2Occ=LclIpYG%-b8J^Ee`S zzOW0~Tjpo|`Id&Tr;QYl>(wiOqT6rkiLZ{eA8P3&AfmpnUG2j>Xo)o!Ev$5!s@PNt%rJNpXjKTsJ^s{BQQG4%xbFV)> za=M%K^|!(3F_l*9-n~Y?bQDV5G>O##OB&~gwarimF-JJn;( z7Ak2*^NzHBH=dd1$Mb9)##^33=0lQoL6{%YgKANZ9PO_r+f$Zx{;~s8FG>*7>UT?D z33(MwvJapiAm+>#k#@YE4yfXH17pAH?N4c%?DDTEe~M>WDbb0tGZFm> z3h)(JgN!V0hBt{UF7vX+9hI6K0!mU%x4A{Fu_Jl5K;AR8PVcd>`$tS?L~prOalIB;tbXK;yVLH_~Q@H zSY+$ikKtPkj&NU5%aT3hYRCNq?D*dQnWCXZlV-snKtNvLKtN3YvUH1>ni;yQ8|L4O)&VGwdtqV)1``gv2L}`O zFl_fKCog8RExoH|q9B#phrAKSp80b;%<@urvYnfI-|_8yb#~3q{}VX9Pg zZ8~LDEg?n5a6%!S*d}Ne4aY~a0U3rvX z>QA_bLhDdNRGo2qdL%FHAqL;3!P3#&F0vZbAGXLt(AOZ8D8Pm~Glb@udo_+^g+&FJ zzBFhLsPk3puq~ekX>~s6^l4!hScCC{=9&|xbCR1xE*}@-A2CejdCI+^s6XmquFk23 znX&q0`$t8WC;)<^B47P_WXD0hn+MvnZNG4wT8oWPT3)*!szX-U!nu`N>v*pDNWZYf1UgdHYNU%Jv{G>p;pEyv5*R9O8;j(wt$SY?K%R=PZV+e*?I8X# zCdKoe;+OI?69LGf@@83S7Ovf*>=huc4 z!rPor#Nv{GGhwW(2vD9?{sL}aspnMy+KebD+XR8AnS2WtsH?nkML#6pek+FU9hmKD zkHurZc4IkdBN_omEd{1INQN*~7G(NoZ0TKKY{}DaUu5B=pZ{a13oB_Bs{Xe_Df%1g zKmKKy|5t}1XJ~7xWNK*gw@DE-bh7aj^{{ji^)NPdaIv(v`ws!1qo6AdB8bMjQu|m* z$3lBiP$uB*fJy@O3ljh}2noM4Vmlw<%I>x@o42GqE6kryh`yy8Qbdq^#q>O#?LD*U zd~$#B2`Yg89t=zGyWq`Q9Z>yNa@0C+v)X^ZZbZhd#3S)sM6Kh!o=w%mRe$)q7SGln zs+jVYL`M5GU=7<^Kju(~Pm1cps}yGuH8z!MePfoSgYot zVU#FK49+?@onM7uui-JT{vz1P6Wpv?@2nUZ`c0JLBzPf(ldfiuM}na1NTdG>T0Nqh z{u)$#v?)3D&utmw7FXiAToZqCcLUT}4T~GSXJw$slbrQgD11?vFb0aS0kYl)Dv{0o z)S(34fw{fLPxuAVr#0+wJa*2j8@b?7V11U=!wP?&Aq^9`>F@X`76&;5Ic+M3vwoJ< z9P2e$Up(^ISB0zqviVLBX+O|^?lKqbMEHCEcFvN2_mTZy?xC!yi-o<3x|5-U!`~hJ zr#ETS|5UI-evH$m7%_6=G18y6q4z|rr=<(xOKSvX1#MTSonIO@PQTx9@fh(k|>HD z^yxA`0y%`9LbPREK+^gN0+QUh;|^0$@yfHjELok-bSATt0Bdf953km<$heT>r;d-* zFY3h_DekymZEak=28U`%uf0Pa2GURfQV=-%3$LP9S7d4jDT6|wO~n)BpMMm@(TgDm z91zd~-oIlB{(C$AAHupz1KKCc6yI-~b!FaOcf~EJio1cinYC-R-O>_v+xU!e|QN@m#0mJd@?x0U5v1v(*NaCm2 zx`4p+k2;0kjBRvj^fL_;w>wvnXS6f5L8W{;sx^YRp@mT*iJ~L;ZsNps(O=$3iB9yl z1;8;7W`$CAw(Pqj^-eB<0mcs&839~nDZh?f(8za8u4Jt%6J%Ef*sKoE$3P&|H}2Cl zkgE7;Xy7IVig~Boc*{f);#FU%(=NbrSEkRF(Nx+jO%B6qr3T*k1(td(nvI!^D1TYT2)EG%djdKSGWGX zPh?Yl9%NQZi3Z-@0OCYWSX^HZ{2x>kQjF}d9t+T2R0^nco`*Ksq-}06lQfoah|P&? zTcrG8I8x^->(LO+#$kvXv*dN$oaiEy_d}J=x*FnBZG0~dxdML_6%PBrLB52FWaGbt z2;2KMe+0dSnMT%=+{z{ZF9@h)+Twb#xp0R;Ocn8%=F1bFGmdH7t&M9)c_NW6xft;% z7Pe4xX6QYhq&jVjhQHV0f+k_WbJy`2D6~C|A_mA73m>9SCdUhAz8jHE0HDV8YcyOa zLK9I+q@@gsSwn~~6VMn8g_73hdigT+@miB-H;gHi>x0P;XmHp}qc6^hWhoR?WfYFb ztN6iUGY0cz4gi)Pd}3&p1l$>WV;rUzzh12x7K&F!QUXGXS(by_0Ws#wRQPZ@F-b*% zE3;NhHA%6)l*)#BJq|o}PvZND)opI}i+C~%3CQJB z%%od=2ac!AJ#T?QDXn7cD1gr$8d=>dWjF~Rj3N%(N5ZlkxCbOkZ-G@a!J8om#OaHY zhR4wKm~ISxM8-YM4JXm;9(Rp{G3hf}M=zJ3{xnzGmg}Tq%4*#ov`&P9wrZD##fsQR z>&hhh^{8u=qNLo`7=wsdNmk0K@wwbqJ%Mx}m`RS1y!?q)w zeY}%r{Iu^+b0y2QEGZmYB|-<>sCL~o*|4A2A)_OHA^zT0;VllZ-_4qb+o917Lcz$f zF4lzX7CvzeU2(s#V7}p91NdU@m~R6Z2_grT!HKijFp~xmZ%NC~;=tGSgg0g= zANzV|z@9!=9`uYKXsG>&=ia43?H5|VQ+mJDsb{{`w|8C?0(MYx(#T9N#H&(@4^?~3 z&5~Gis8;>tui3CsB!6q`T7t>ZLXS~<&(ndeCduVF5Z>+198P&4Y4C`KuPH-(MfRi| zFnZ|G+))zpyoC86DZ7e>n9DZ&wCHV|Kd_qVDQjw%ggEbf;iy#AHBcu*KLyCr8qjXw z8r&HZ-5dDiVKTno6jm{t1_`6xAEFp_GxvoECo4MaCEH>JZIuT4=CEI0t>9b~dtkl_ z1I9$#Z=e`Sb#Eds|4y&>Wh$eulr+7JqKr^Y`&pldl`NOi!%BMXzK*+<9>vc|@TAMv zSZs0Sl~&x(nOUxCdc4lRK2Jefn>xCT*lW>fYANSE)ko-R@T_@#fy=A;^lMy1{>~OLD4U#d9B1${E1jJWs(Me_w zc=(4p%q14SEu7k@F+@@uO*5snVHrbWmK;=rORmGCRMN0c?fXAT&nAykPBq(|`jss^ ze)p+U&va0)-oU{cer=j`2hPoU{f-)7Wytm)Jgei5;Nvek zV3;R9PHK8jwuKX(ayNg}MD8Epcq`ASY50Z;a#e(MU=Ih+ixZv5k*u)9d@bPMbxAF7 z#H6Xn+z^b5;*cVKsEyGPU{MGJEIINtjluxMVKskpz z4OfCVkM#V`?(N8*BYc_-i@SP4^qrQ`y`U-)Fv?DGN(=cmhbbLR3H^Zez918gHH3$z z%YLu~!ONyhrO;#B?AM(?`k2%mA-OesmUY$A9zMV1_^oUrXY)s^9%|}yt2<5SQ-Ug= z?lB;I)(F>$B|PI~&Z<6MA$MK_BD)`;mK8s<+8f-~9IG(E>wIiz}%6* z3KWXdved)j_@rzcR=u#$@5s$rTE&%UTohfhqt275+rx)*R6ck@$6hr;VCxbB-3!A= zlPaz!_BsSfgul>*w9#I9hfkKc;^}`29xQ#m9d?1AbcvoE_%KVS^6e;>socH_^1V}V z_gJ6bTk;_uqxEE&#sB6m5@l=Gii_8RK(t#i>f7T|5D`H^T$g|iP@JV zaWq1cj*s1U1_^oE1jyx@l@J;*qRtdgx}*WZ8O_-{K?IE)xaK@@kg+J(IgnE}BE*Sn z9klF_OU&$YQ}3#N1iqq}XiD(PN+<>}qA0MRpIhUyyuf^SGi0hT`EJCpn;(F_2tx3$ z#Z9qm*blGA?{G!e1F)jyPoVs^k4dx;Zw6F1R#^khL(#z^_MLKHAD=)&%4~R7%lIXl zt>DmgiRTr?f4VE6&g;({;x}bRia3Dy*_$`b=>Wx5)S+8FKpL$Uh|Mr-Or$r}^b&gZ zm&>e)kp|X1?BW=GYt`t*O?58W55*Mu!h*i56@IegJhRv@Z$6`JiL2X|Va#$li-eD$0eZYuTF2{jy&I`XH~X*g(D^u;PaK;C zooBG;#K~LwO(-@#re1*})`N6er%h-I zFy3CdRI=-H9$@qbaO(821zkAFM;|m`BDP|aGaGe)>v8{*w4_}ZdSJyy8M^#C1aj#V z8bo=gC#Dhc@Ws;B6Qe@O30HJk`WRkH74DYuhT?{drr0H^#n-K{L8AoZKB1P$F~{sA zS~+K@A!qbNlRQu8S&1!d&|Gqb_ixS*|^P!@MJGiOpDGo0?7 zB-4J=qAllCz^3FxBfjMs_Rv0D@c%O!IR*$_)%}It^NsN-T{el3}*Rtb2JJwrCQ2}DUzBTJl;YTx^=g8V>smiZKHe; z{zyL=wMEZMkVJIfe^-b+mWMoFRHzf-17h%!;z1C$KhfRX?fBiSd(HRje$E&OJKIpg zlZQ3Pav|3-r_(ac$1b*A4M~UYhvDew7a*)dS<}9~C;-4ZqjjHprnMdINzOmLFZpTa42|!LnSBBx8>XH8=ip>6*um8ea;qCco}99CD5cFTC*5ok#6`g;T%GJ zdiWjru-198TdSm14EVAZ7|VXd`MdFQq|ZG`n6~%YrW52U;#Og1HTG@knHcbpt`BDp`n53XkyaJIxth`62=k^reH**K-h^&(iT%G zHWN9GJ|p8DK0=eUKt> z0xO=#)l%17zdFn{hZs(&e>OG?tB6I^3MX~JS3Gk&sU5M%I|Z#)aalZi7j=4%rDhM- zZkP7>2A;1Q6_PiweYtXw)GQ699gcdq3QK9YUP@QU1lduT-Ltb{E>CY@=^eFe@Nf(< zGA~h6F}p-}GX?}6-r@!PpA%xf?U=6$^$X7n>)Yc>W}i zZswb4peG~3+8EpvW9B5@#6sCvH6XDGgddC#nz=LACXyCQgg&%ENIbT=0+YH5!jluS zq>#}VT{n?TW0ziWJFBnJtv#}7Zn|i%u|E6u?3rnxlV%ObD?ahve)he(SlamMcSMFv zzl)hMPAXQ&zO9UV2_M#O6*QNwxKVH4yP6FZa)4k&@hs;Xr-!>t+SO#M%N=9TFUVe! zxL5$*Fmos~_#n|qiE|Ohk^-?i@{D1?k$?{qW`s=QV(x^b<0%VJUDkTb+eUAd2tc~Ge2#BIr-%7P9t;Kj3D@hl8C zik=E@ zPNlbl9ySsL?OH*e$YjJTbt}|{y8eq2u4$p0G4YfItqFzG^%ucr@iR_|!;P(*xLG|& z66^}DIIHuVlh$z&k5(V+3Ck;zn~s#LlW9tD0Za{x6NC~n7P%J5*6?;S!Ga7uLq#cs zqb@0`SqN=aGVwML|$}Y49#T>9cLLcTD>?Di;KOy zV>jB;kACngStrXT^ObF=OV1C35*HyEJ`JIY^#s`o@|}|CGX1EF8%qljlg3bqdI;M%b)(#p zawoDGkRo?4c~}LU$&J~T_()oX=h&J?c2O+P<_Gwa%%TBn)6eA zQjnN(GK(v#h0fqcvi!orj9Dzx+0=1Si1Z}jjW!XMI~13p*`^bjY!45_-&`VS-;C#( zDc6l!(>ThG%1zW_Ny`o0babs}MV=yYl@m5iC_)=tkZe8mWT6d3asu_Dvuh0VBK0z2 zf^akcejKhD4qQZEYMqR{EF?9f{(^}n4QUZ&(np`1D65N94a0_8%M`9qDm52RjExuz zE1nSq$9I*i@Om4=fQ?(IorRVR=@AXX@43!Tif@vSYau~VwH3en!1t{V*ZWl~olW(y zYL-W%Z$G;;(Z<1ImPChcuLJ!ZntbyUXu!|6S9FK9W2>bh)}Ola$0=dPu^B)KXf83+LDFbwAqlOD3NW>5B3xMlWqIMflp)wU?? zDCx;IvY5)9m0O_ZxGX=+*3y`JYwxsgK59Kw0>BY8CWNaX(#Yh7|B~u-Y7*<|U*U<_ z9$CSN;ae<2qVAx?Ij>+~pdFPex;_4zIHoyLVo#s9G^*;DYH zstDn`D*8>Dp(fYkK9^#a@&(%b_L%-4&vu4^)dBhFuqpbpFzfIzPgf=X4FE=3TBl9K zu9@$cJLsEQrKmaVrgQ8Q!5U5dN<%D2Y`9zydaCzp;~}AfbxxyyNVH82%!D!Jp5bYaN@fpISQ^#ZxVx<$=sZ1B zqox=-^xBUPq{`ys(`Qh=&p-MX(cq3NKN`sTI6Z3E5Xouqc#@Qz=&zI5>{B zB>i-mpI(xq$0-u)EAQn}0YZ)r%P2E%>aC$=|ZXmjTwR z{Y1hc9&a#sG3E;-!hA;M1q#CFeyr}84$?*c=KX;Mi!)VFC%7;-c`5ReO=Al&tK#{! zw*jd`%oXvd&ZogB11011E$eVs)!vfH}t6Fqz%RnL~3rH zOC)~+xbvbQFP}Wo+pUqE&j8|GxJ@Wp8(_VbfOyS_AF@wiRKVhwSef;SdQhrEv#Hhe&Oo6wX7@M2%b|eS1D=!s19bNbG;)C>N2CuMh&+46+za2t_F&7>B^8m^}n zMfpCF{b5ojSN7vSE9apG<|>qhb-eawjbF zu)x@obQi4Vs(ajWiBs}}-hvHBb0IWg`NI#Xg!E^yT9-+N|4OVZurS_{5TpV-rF@)67E$A)bS}uB>1vM5!WB_2f{dW} z`>0nB_(e*>5q&qPJaWyZt=vdvh{w0QDxc9jdHh1)9*Ngvt`meuw0W)0i*KM^fx{z!5cl zCwfcUf@Di71a2%Xn^}%=IKb-cy(i8HBCEr3iRDv zDZ^AON2G6Sw848c>YCZ3J3hDg#OXqxPwktm>qhf|103md2<7aJ5GX2W_8ZBKFz^de z_&Y5B`xg>et++B$Zmcqn-1T!?Wui;t^hu2v{A-pf-*YF7Pc6IjI7~>jE zHMC^()=EXF<~N#4Zkn*O7e}uY%aC7_j@f$VbTW$5AHb^$p->nrj_gev@W#lnS zGHEJxI*f)CZ~w&mhmF3vvsG?5*6qx_TVDX8#0jT`AAp9_(sS z;O;3f@ugN*eS4HpeKRPc`5bwvc*w)ve#Nj!EHcKJQZMK^yn3ZP<;YnFyy;fX=#rk* z4f2iog~K4x-uoUU&Au*Ep8Z4y9y$BIv$!DZ@D<X z0xMYnd;0}r8Stf#T^^5PP}DJ+x=hm@Gjr|V?{1iR-B)~VIR3(CR!!K;$}iIJ0afAS zTom(r!OGhGg=b3gN3qwcqS=mwGyKa=k%_~^>xL_-QMJ3sG^hbiLn0Eb(!dl0<-~-* zRD4s!Xin&#$~WqA(b*bdtBp~!?E!VlUGFZiMHJ3;4!b9%_Suh^s>@J99R9~&e1Ey83zFM02va~I=zdK7LDVPAl7#< z4!4s2jxo6FmafiaxI5#{FSd*yrrA?UvzVm-<==EOB#<*Do;;W`9ZC)M_4`tA8Xuk5 z>I;h&0yN&VDxSkFwxhY_vsI2fA1Xtz77%$M5}f>OjI}Ld_teJ*Iy6Qd*5pAm$pNmW zC6~&>bGmSAY4K@k0e1czCVX7P-rk8V9(-a$SkH%t)joSzm9-!Va@$X9ls~#(SY%jz zp!KCRDSy;Za&zP-)DmNI%!$~QNcqehx+0lC(v(n-PNh;#=UA1vZ&81cDuw8tkNYH2 za_lwH)tX_CBYVCIgA2zJ+m7lo9lF7_H5GKqe9oBiK9(tiXMn1JqB88N{fOGIAhs8o z)fbTVM6KR+(;D(ncf^bXlc2pMg^|lv^I#licHf(2Ed|Lw@RT*l=~&od+JtUu3D|a{ zIf@z`NCRST9VzrJKk6OF7JY^uD=fff0B;R7L!-_2;7O8VI%`T#e}WH`Y(auZB49cb zReM(Cx(2I>6V8a7r1lh$NQ0ZARu!;x6LpJKB!eLU&2PcwG1?7)M7Z9s6MfG=>JZR! zwUeuywx-LI45^`fZ4%r*6saUv-eZ&XMC5Zp=!2@OH|~@Pn9Q7f-)*JcZLPH6`a81o zq0jc^rCITFDbBLM-;ud^RU2K)JFk(g^B-KWCJjooO=0a@LQjcX_{|3gjL!^6H{F#Z zVe6kJc@+(0nnlX>3{Frc&<$o_U}rOS>M2IemfyHtIeT_e5vv?P18Us>y8s@Rb%5>_ zM1275oCK=`x?<^*1gd2cEY+t-i<+AIBlMl_MmHeIg}+N{2*w@lG$=KyxBl%hP9I+n&;Tu@{hi7=jH^mdIP)~+IueXeF!oMCqC{QR;4Ry>ZQHhO+qP}{mTlX(W!tvx zn))Z^r8|1Qzat|f@*&^O$$j=-xiC5rS44yNaF0}eq;QUNYO08J%_51g(iKSl6E_h0cZ;)+z2kY5f@ z;4dgl_df!{iYAu7q+l64V+&IY6UYAzCq5=&Ms|ZAexz4olha`{J`^#Gl*WDnkHXhK zPnVKFB)BDzGHjc5n%LUj$l}2MP{194Ry&@*j7OR0B_G}hNdQH;JS->Y&Ga)X<0-c@ zxA%{8osju7`}2trUwW|TDOiHRPkbr%5ZIh}97`5ms2Ha_!v?;fMTKk%Ud7tb54ZHV zwJ}8VNqjG_mq6K1(3*rj@IUUNrO#k~o`DIS->w!caOso? zOh}j|0E)F_)nu@|uWYIZpD2;6qe9+50HY?1(03;KfTrHe>jL3fgK~DExdGtz0)dej zY74#{5m{1~|K~vTD=ehpjzklggQ#t(OO#=yYtX!k=7X;F5kb%jIZ$~TJh;|AS^~-i zL^tJnN!f*aqwzIehVBO&CEso8Y=`LCC3cGo0F_t!e4m%~Zhq){g8(Dryg65O>&<_o z+%eGT07(2UvF|VL|9{mg(fZN?xu!I8NG6czb`me*pABXehmjA+#wX<~+1e!?g(X+PpqRIDraMLn*1`M&0{iN=OykOH0t# zamiQx%1mKsp(2EBSE(e9lQIbdJ+&L{ZQNnjvq(~e2X*uwC@RJ=ZQ)#kfjYKwK`eEU z#1NM*%`PLX#1_fKwNth2ABCL}9H7?ti;vhXew>L#zThvys$m#}(OAoe<>HR?I(@9R zFz&IrNE5AnD7W4MevwTzz-oCnX5qq$Ome2zZuHP2kC?HXLjL8DNA2^3>SNroL|PV| zYc==Ny9zqjr6SHbsG$q>d^hS5SteMWyKyBjCoXW|wxb7fM1+P#C!svYzLNP02#X*1 z^9A#0qrmBlCau!O2K2XD@gpd*^zY4B58=jf0=OE3(B*&3qRAp3f)6f}uJDHY60@44 zct#!ep;?%|h7sS6cW9;?BLfuIe+zE&y+O$WUWVm#A}>8G!vwEX>qYx0EZf0G<&z6! zkU*M@ikgrn%}eW=?G8QWMd#}0V%d8^=_N%-4WiNiJ0(=2_0v7}SC}9G^XCus|5%2k z?TiepRSg_13=FMJoRsVwf5j{RVV|a`YPlh)pl_9CSgfPx#{@Hz_7j2=%%XNF7b}8{ zldK173R{VA$&{QW8zY*P6pM&dhOZn2(b~0XOL^=G=xRfehePf7xV>HCDZFqn!O++g z^D>_tX5V;!Yn4k?6w2@FqMqdzGDlu#l(HQ3cQ3<$ z8RFEE3Lc{odqrskkZ=ZSrT6smhmY3_7&Euddg21*bo%&%B%-FlAgu$G=U;;q&llCH z!=@um?fIiM7i}GdmsutGasdk<3`0QdrKQm(N|!d80Mow0W(>=eIlmx&5AfZpbPOU1 zh^Ayo>k5U8s`N%u7_G}Rtx1cGyT#&s7g;3oJi=yFG=HOw><6Wo175)tbfFHubg;k$ z^AfB{>rr7FURKf9G5K{OE6nOZwmM)Yn=OJ*v$F|fv|zvUnepNM!=f4ZfekVI!Ym`cPVGw z-W_=Em2P``VEmvvVWcg$C)%_B)2ezkN`(l;eezy>OeJ!!R=_b$C%!`A>dnRrgBmvx zbK`V(115dR7CG!2Y8Xf+%2vT^2IswcLsbT-&nO41q1e}8kGhZyLpB$RX6bl8HUiy_m2&%5M20a#7=NU7#?n3BVSn8Z*@a{TNoPSfj~b z;wnbKrr=U+tZX_+Em`lRg6?usL_K(ah zte4A-f-$(#=~1Nl+2O;oGs=gSA4ik%(+7#>{C7+6V~+J3i`xafON8ampaC$-sK z`%@^^Um78IynJ2|OZ^U6Cj1@1!>osYnm4AI@?E*i7xG-Q)xik@crS-&K|NzjwZ#8m zV{yu9Irz7NogKjA_8T0w;|?!aN0}jyfdM57J(BT|I&IJ$7>p%ZVR4MRTok~se1tyr zB|2vpW;sa;l%FCnBNTzm6u`{fg(A`UzXb?LMD;AnonOg*Ta1&5!!^CEhy^`4w4ZmQ zxKqYgz-s`5!pphR;bYYa( zlik9D_4&#I5}eq;U*Ug+wzWu}YD*G1zH`D-2iv~DoX=K6%9m{kfY1OCNj3nIP~2b3 z3z&!x-`QQ@Xt{!NCfywc+DG=A&?_mNyZ9El>dWQm9B zdB+pr<01(U%U|5N$Zp=(h^#G5(weXEY}}O*ru9*l=x@xmYOvP)BHT!;6?)oUvL`Bb z1Z`tZ6Sv%dss2H%^E^MKNGoyEOp;Qf4mwo2nnicA&r!pf3o4GsXH&})rm6@w`i_<9 z@+euyGoupM%*DPMFrX5?t(huj;@>vsJQHX)^Upl6agD+rxjgoh0mxRX@ko-9)Mo1K+g;zzAK6X4_*Zlm zFDNuUrU|}-)9iAZI|{ouZ`VK}`AUFmR4~YTs3u5T;kW* zj_`jx?0yOIq6S94`kw#9ldo2@k5j$^r;4H3*nu%w8aP%i?Max< z?C5DPu7^IGSdyS+@6XUiXP(xqFFGL$VuXbDSakNstc*(XNgN=Q$F1RztK94D^$mA@xmp~lPcp2c9C8=Cwk~#p-(9hG*_6^k=TNg##HvinOLzGA`IM1Ozix- z^3G}bo&6>7*%}dC&J|Ybq`S2@v#%+=XQF}D&Zs!Cr=g7=6S71!++)Z=JrhJ|NnFSL zJyZYdK$Wv09tV>j9?MY zSaj;lhxkmI+*|Trx%0Qka{XIqv^&}+DCCj+$g z?7IF5eWucdw|fE9oG~s^rw-F|=O}_ov}su!8Vg*=z63@`1shSfJ%Z>_P)?!R z3UXAIXVV4{v){mE&T z19b*PG=HlL5M8k{>Lgy-0QX?*d}zX6F3SQ2CSly%Li&=Uhfj3`tMowgbeq$T4`!~z zzJqsafjq{GFMEbDaxNL!S=Cw2^8JE=k)p^m2Dlo+6#79XhReK{_QX&iK8>1$oZEwvB&l8Xr> zdPhsoS%)KETRyJ5r_a*h49rvkEEx&!2%7v@0y%^u1-wxM1{lkuDqYv)qp#0_S z_No`uUGs;`Z2+Pc$}L}`WwJKge#|DSxmy+jLuWK|p(8_9F>d^R)Xadmd6I)BZJCtO zQyb7HAap^4hm(yd{{#~Iq>Ld=>Kw?T-2ukb!m>Q`!b6K|t!0jm`7GQNv0C#GUU5+; z?=K(9)^NJvroY2TKx-F)t+C_qwCRbl4w+jU?7{9SQXcM{E@pGy!37vSM~gT7j}MR9j& zo)5P6nu{wf!RlIb^p^-!%(6s(pf*n4M7>K3{seVVqYFsvkjp00)wSk8_trJ<*OgxrM@Pn&A=p5y)8)R7T7#i$t68e+}_dN*fRU?W7w{;CUiLxr<7 z^1g`7$rmRF@b~&O$2$fhp^I-ZTz47eE{&O=tJ9m* z(cweZMAp$EogJYZir?el$pEjs4$yO;y86foDg*Ifd1lJL%FHx>6`fVRHRih9Mdv2= z+fBOK!jnJxWXu9PtqdWuyy#QQ9`zYg>%3OwsrPP`mH#2C;OgQ@wO!$E&=A7L6~>6C z{9BVMT37N)2w!OA5iA;6z$DtgzvK}&&N_+bu6%+f319Rm;Xr78rOX%ej*$?5dH>t+^= zn^orqrIW$n!_DKEliLJn;qNW~J4Cby(7MQ67W~Q99bLXX`+ufU{!im7F~fftSYxEQAP4x7v&|O0o5Zcm2nb*Bb^+{qb6mInebOmXdw-#YmIAg9BtrUu=T>TCo5{wR~|5zgDtqnf%`(PLeuCk_+y;H>^EjpSCWdf@PU&v{` zWEJ(C;mtMjC+G=}cjoK^x5#b$yQs52O=9<7aYO&>`ET%FHvz8yjrq-g@)DR*SyM#% zRcrF_gC-gU@fsqpyNx#fD@obhgPe19EL`9lz6yyNU)ZMPVd*if0I+sdL#X-w?g(riS@$W-b> z<8XQBVzIi-k8CW)()*ymKo6cI6t6YArP`SyZnh|GZmP@;&AK8w362=fUKHybar{N; z8Lh^C)1`SIAHOFEhJteeKe)B=LLN=M!3N( z!m9hgleu!(rBN~nUyD$O>L3UzCC)_Df<9uoOS>G)d9d!ta7+rH27MqXAdxE$un&iUq?A`7@dswMOJq_S{X)AFk(8`wAEYd`3+MFtu$nm#O{k?@JpIN z%76?#iRbbzI;UUgfgdB#B z{T1B_*|W#X!C*PKxWD))m(C-DL=!M>QN$AVW_V7(O{gfz;1cgizJs9Tt6Z$dZ^!XP z{e)PYvhXji&OJ3+RiFg<4xQjgO+*-kE(#r`whS(5Nf2-eaCqs_>ifURYopf_h&z8_ zdBFc)6PN#D*72VTONio}?Em?|76j3j4fBzk35%CSv~XPqogyMA2pBAy@>hfZUAcD1 zHcpZyZMu72f*25o z>cUf9=iE9B0X_7Gi*tdYq7S|g<-X}2PRx4oWy{YA{>@J<$OKa&nIx+%9g-I2J}hJl zK&Wr3o2*N4aY6zR+LISI&}BNzsunleH z9;}JMS+rQpaUa4M-(tTl45$s}i+;60JYB4b|J-`?9$kVld^sqGe61H}9d1>0OnDw; zIUk)nn4>w18=os!Dbfk*;U15^zyEgQe$7nPlc$~*7`IBs(F{ShQ1A|)k7*OS1g`cS zT&7DNkMq<2xCGS7Rlq+)oav=TSN&n!EPdAWWV2Z>`Rbq06obhg=s9YM~*CFzcje@{6O=NuTMqxCOXc zOeB60+I7hjG29JE(S0h6JfJGKD72rxoj+v%eZYQwyV!91!{t6CZ||=EiZjT29ob^g ziOW`@Gx1`@g8;lqrHZHz{RWk`#c#YQ*HF4W>%9(D8r z{kDGxe)clc#wJWVs?EMwVYIesrtX0?CL>X0f)s5(X)GUvw1?(q(ft17M%2(jhK;~& zz)m9rBnAzy5SzI|-HL81d6XK;oT?a3RA_(;B{5{L6cia8ZEKN^BT9vtC_ZMcGKkHL zv1J-QfrG(X0O_8EpC4*;f?Q8H=YbyGv#tXHetuTJn7T)cB>@ zT)x+n-4YJwEO~Aet7F@&9NUxgc^cqF%o*tATL!g7x7KDKh=dEC^<32<#w=5Ey{_77 z;xS<9=Ancc>a4^5snWYrk`b1oJb2~B>PKK)kP$oBEe~^D$lo!1##T#hS~RM^S^%9q zr8_QuSsx9IH*qTM$4LHIR4@L|D>Q7)Kx|Kj%LiKOF*?UDmP~PNpcf2ia~hdi%+uU zRhD~qYgszkEITunD*u;W)b|~oZtQ$Dn_u^XK83n?6Gj1&^6-emF#qrs=D%)25bHH+ zVWK~OE|mWKA^IP&3n4pWlmE|L>OZtQ*_sgU*u(WTxHy$MF*H*8Ya?J*(CVqdAg=JB zT+<6CU9r>wwA5JYqLm5U=FLRU@iw>wn{1)>AWp;DtZ@tM_&}`be*gsi`P&HK5d{Tz zLk{kCL&N~zD!p8r=C8*Ib9=7Jug|_aJ1TxJu^I1&St4O(Juj)*$#9I?$S36x&p}l+ zmC@Kt6&KgmwOPC21Jgwz!P$T_9n7nEglqbO)HxJZqiR>>=P3kc3X|JUq!0 z)m|3cAsM(MsPUwtyas5X0a5By?D|Q@!L0!CxGRF%JPo6lXZd{mS}x();PvCV&wY}mU^r8~2oK}*iTSI(ikIR+o{v(htUc|ZU_7+Xi{V<%pMh&mdgFo^ z#nd(%?%G)|37u*F=w~t%M*)o(1Phu%$8#`LqG^q4<1vEmr{Wl}&T_M0g#|uOwA~`p zs^6ge5=ZVGrxRD|EMtcSS43c}8Mh+qF|_iJTDHJQCyuLC#~nq|Wi$pEm(S2C?*$VH zOiXAEh;TKroM{AR2c#vH%Z#kwyu34bJ*86TRI}{{@l?@%wiEqeHuFHT)JPHXVUFb?%~+dI1|Rn|_?La88xAS)Bej z#TEdu7IYxOTsh+&?t80i_a#~{T?pCCn-k%mY-5MBQGSOJ&T9%suw{Ig$)PowafbLZ zc0QEeaKY`+mXBDp)N&TWf$CH(ziDf*c6^_{!XR-V(NPgso0T&VkL{Pn8r1HhK&2s+o-mq@zvaYJ5kdCj^NjSv=7dG_IQa~Nu)+x=MGd+F~y~Q{dZOgabGB8z@BSsL# zqXXwEFppb8;zn4gSKLcb4no-v1(d~K=Zp*d>CLHm5HxWUkI(bwT3_5si5Mfr6E}-u9P$nRji8%q4rN*YvZ^*c^FTfUk7|%NEMKS`qd_`Z6W{YxoTZNr_Xv zvMwwKM`pyeHciC4E&?Z{*(}=1XZ!-!H#~9~-uo~1{Xjjq*Wa~q0uPLoo!&ym z67+IWZ`LUK=d~83TxuXToas zlq08Q-n7JbkU_5FoSO>K&h)EZH$~A2)OwkI9Hh*dtSt~R#hW|u0!{+a{O%TN?NHvF zidoQ=7!{5(i-S1p{NJsE;@0xm>h}$~$d7I*+al(7-oiwrr(7u;%2SVQE%`YeXLoT~ z?>%Gz=|J z-eZrt&AwDx0pfn8tptDa_k3*3vVce(mp-_g#*qRopURd2rRO@j+J`d)UzM3+3EO!T zI`N{SH>hWXp&KY=*OcKc=$<3ehdzu)$s$D{)Z?`p>bZ(sj}E0xxALR9hmEb36fzm2 zOgx(ZRohPzl?-eGx=hJb+jOjg8KNx=JsuM4`;*NLOJnNSX|SiMR~4Z!DzlCoRN&4I z`x?T=ZR0_#(Tl~n7OfD$4gn5X=cZGQ*O7t7*aNO3P`RHnLNrf?XpYE(MZ7_)gv7Hz zH$%V2c?}UntDGT04GHQ~A22*27H$uhnMDr{5st89|C=?4_T&n4J$7#}*Ve)f?@AVO^;)FAE~R$Tw6V%e%yUPrL#-^UO!# zqO>HiY5<+8pLB1g1vCVCh!yV6E8k1cOij(4t{2sBrV>GlGd~6orwF|Cb#`F(8Axg+ zW)oeFe!LHTyjO*E)T~hglM(q#f!`l!yj}53@TyM2iQ&S6_hIv+jZ446r?1tu@^wMI zNc5eKy|4DB7r@SH!7ZMWa)(_TO_rX$9R5s5m95WSFL->}aatg2N>l_TbQ}T5NGbjd z_G2L1o8{#h&wgfDy^qnN4u8=I3h(Ma`%hK~$v%>oLpF94`%uOH&;}8K6%I4Lrei}J z&dPu1{=g} zVGM1}?3svb9|>`q82 zx0g=vK@Xb=Z7mlRn8WKx#JoGso&78);Q`AiyQVsyTa?%8W9!|qb3+yG)8Z0g^=RJK zzZJ!dD&N=eDfO3#4~-R})qnGei9%>5Y{fhRy!g1rqhat$$eQxUR&&DOv4W*z-c>8W zh}(lBX&ol%hH0x)O5Bk}94^B|q==o$y8_2tBwsG_I%BsZX{B!CORA z{nZr17MYi|M}V&QmJc#|^r6N?!9ym#Ynjq+Nn+);)eUMn2(WG#`Ml)BFJQvNW#^sI znlL<;Z<%Qtw|D&$(flvs08Ss1FzOjCq<$2>Im+d2qQLIR~ z7jy%U8GHC_Kb8!?z9Q5&cTC=m9D{elL+t;A=A)P=x!gr`-OQB`vsnSId8agOq>E*Z zkU>Sog_)RG&A(WV00pt?WbG|H&zVB5YiV%pI8~sK7uM238F$N#+V7p~GMj%``dUNU zu{|E+_MpYST5LQFopwr**o3 zYQ7_6%ePq!Vo|)5>ljp~62}h8B3$UYSV$A7CT)Rz?E~Bu@rYNqWTBxJ6^$AOko=WO z4P)Hnp}>FF58aCqv&J#7yFa1;op0l41nRqbio~FVi4?E)kP;SQlS@SBAulZOVTic(0T5Hi4ggA>@j~gXPec(v7k=ZF4g6b1Q@;mZ%%q3^8sbgm!8dZ5vBHjg8p?tFQaE3V{M7 z6RsVD#l_~kN`R+x;v7;635Uz%oNT$$j#POaP?yZIj;fV%VFj{_rnCatxpH-d((|U) z0@Mw5>uw;gmTFeS`py1i{ou>$x@neB%K;7G|E#dmk!Lb#l_H5bMuQQWqV6PE%Af9y^-YeDb zytFf4!Z(hYLYpUx);)PKP?!8(*G|Tb6xY@eAR%Oe7g=oP=8dM#LmCnMjEu8&9?P5! z=exwolBO$e+^zeh9WT?XvqHuEq^9z(Js23jy2;0SpI5A+yfv?QOs~tVrmT$WoZ0?F z`PDa5?RAq4gN5+d95b+7?TKR)^SC-AwgLvGBdJY-1McP$@2myfCEtESkLWA6YJ?}J zJ=uF;&B5OTdnRP#op|4}wae$eGVJ5n;;@#-yY(ooc;h=-I{Lu(bF;+^L?IxQ*fLRD zR7Y~4wmUCfFfyk$RUxk%cu-DP$))VT%QlBQH8uJ*`>gGxK$OammE^tCen^?7N0kc! zCS!uS<}?6YFnci`qj0TAfp)&q=QhlTu3Tcean&Q0eWpA6WP94F;ek43#)3JdBym<3 zpgNxw#xC??K_NFmF*oA9D*3&q>_h3iQDKL!m>2BnOX(ae&sSg4hxXir;>n{dt#pw} zzE-*%FZlg4*%Rd)taK4hz(+vRhv0lisvIxU(?#-&{u6a>FOfG8^})|>w>7vY4!1V2 zBb_@r?gZe7sq_d|LFWioQ&rc2kA`kX;_q}Mfu-9z(_5v8)&cm*g{Ik-K39nd-=eUz&D7oiN+1}seJ>K#h zs6S2w+r#A7{M63)U0fD*jox6@cvC@aF<~*}Y<1`){D9gG4k~@(az2rymqiPlQq!vf zW;Y{|(;^2Tp9S9Gv^CfT6Szf{59aA|y_o0g(jYbkR)@qW&NMn)9wpYV)+yq=d_u!t@T8S$Oo1b&vh6hK1ADJX;E9?8V~Mz zZ>}9qoXRaW%ndUl&+Vf<%!gX~?5+D-I?kvj7^|t?kI%LL zx;ok>hK+fE!OTGwdiRvcS^_La&2Oh0`j{sg`t(T-3BJ>;<9j&JcLPhleF^}p3*KOx z^XA%yncn>AbtAmoifOBd@qN>UoSPXd`Pi+l_p<=Yk%cq(tAl<=FM26DQTX_W2Xv9_ z6z@?gK}rrX=PzQ?sRcudFNx_alOZb)6>P!DkoS8Gd$R7IG`h~|O3duYA{04JQA)EW zu!V)}89RlZL=dATb_QL+H%rv$X$Qlu==)}B^nbBP2Pnz4X8r0sve9Y^JqaL2Q@<|> zy_up$AB;rYagMh(=wtKAMzhKDW`Pi~|2`;u(~|I8+B);Gu@MP-I{QgA+_eA!w32RZ zE4~i6Ik7EFLOwm-xo$~5+`ba|`mIYgX0stFd@=drSr2y!JrX1p=A0_7M9>z;Ek1)h z6=2;T2hkO{#rJ&TO00E`=KO?XzwsP)`H8-MKyCZ^5Br20-w++f;~9PWbw=?{YCHlT z_25plT|!t)d?O$2Gz4(-%Er&g`%34s<5T7Es|?|%EQFmtD9}ryi`39Zp8Dm4D<%^{ zgM4v})169zb!Np`_wE1SI1uWFX!g)k zQQD#CplqC7&@@#!a``ldNG7Y(sSrpoE8C(C*mMtBR4XpUG>qT1If@@zqbN}k0~7gN z#kQ|P*y_dccZ^#2=&8_QM+ncTjZPCK(Atk|as@6taPa`yW zZY@t_FRKuhaIIKW<1lP-$XXqRA7x)Pr532{6iim6)6N(x4>$uAQIN6&%p)V|6t0sa zh5O-qeUVkYVP)&~7JtoJ{Dp54Ka}SI)NC9rh26_P5C#YdNi!S}Gs7RX0p2H75Y9rl zY>70)L?n5vTZQVkW$dPvJmX=A;YI$KRr}OqH4G(3WD+S)CxM0kh?LwziyF38pn66U zi{da$S=DonVnqDB8)X>&hfue+_+DbgHka(z>`1e#fTH#~nX%Qy3Zan-k~#$h5%1`R zgdf@j41P~9NCF-x*(YYM@Tq*OP=OB^iO{?}uSpJCHLt$8UXTjRiZ!>}s9(8MaYfb~ zS>2*(`P6u$N%LiPeYur2g;=-yVsdl2<@NKBo8dH_&BRpJv`O_TvnPviiWsB}HVJpK zDF=}AO(I->mNK?i;PSGeqoRy_jc9_%R$b?}m2jxzcYp1#n|`M1+irA}2Gx11nqyji zDJ48gRe(G&sT-NmEg4;cY5Y4jtG^eNm5?e3r4axjSVl~fH*F$AsF<{8X;QRO+byVO z`DzG8(?7zLMwT?lGEfqgQids|)b$OhXhq&7)18;8OBYEh)S8IJT%wdPS&jQ-X_f1F z@bWrMK(S(h9zigakOiHW+{-E?B?!a}=7BMpRHBWWodDVI3RYi~45>JV35QA)u~QItjWAu4>H|;vlXZtk$)ATwojPytKmknS z3M|7M2uWiRr8$p-mR3ZB`drY39j*pwQH~^i-2bjT6G8)oT+@Q}J5YR^Oy%sKKgz;# z6OQ(yLTqC=EmJTiBCU8pN3<5o% z%HlZAPLPq_oM;w%#Vh|xhfTQbI_gN2z`U>?g}eQEQ8qO}8EkVuULEg>8x|ZWJ3{ z^VVkf1DyUyzd$OO*sWqKQGqGsgomFQ9*(L$ge#Qpj=Q&)uqMl31m{-5D7DbpH))kv zMOD9g6lyj?TymwJrCC~P0YN>_{17^f@+++0*PFdS847=-0pW8IDc}jMT}cfzI`hEX z!g2Zzdi{Zj*P9B*<^wn+O0J|mr?&t-9QuiyL>$G%{~2dEQ(jNV#y!|?=ADmq_Bys6 zKf&h5y+pcyngKimGVXCqh<d;JiGD;b_qW=oqe1FZpk(;jJ8@qJse5Zla=thWGY!{2Sd}lWLd%`qgz222CJ%r+ za(p$=^M<3@BuGPNv~j?}VKp-9W#P%GQ=%eyaW9Z+Y(j-Teh!oAp~LU#uKMFaUH|iq z9Q`2{sSpz#58DRBd*`m&|DpWPZ&bH^ELr+eV)uSwiR@jP5w4tmv;DK0@Lbc&)l#Y- z?#CDB*Y^(2N*$T}k8XX*m?9Rk!yUurZ4z=kk8Dj7osUY^lIMD= z?&pd_Ok=baFrg$1s+J4wqcABM1p6T%tz!!Ui#%?wz?TQ-#PH+-=i^eBBAxUS}@}*Xz{wHB+&` zjr_f=Wa&tgp`gv}B-KrX=^t&cWwdn3X!pcz%FFM8J^HFxFpx(gfjyMmW%0+&ufmt3 zUY@;NT13+`UZ;&f$MRBdUg2MbT!#Y9)=_I|T-`pTT% zn<(diRY;m00`E5dr*|d#5Z2sBYn0(Prsof=1Ee2Lw_hpx*bcnWYu4<>%vTH+EvrKP z5EFy7OKX4KfQl-*=%%P{dm~85Tf*Sq#mF6go{5l0e)1p6xh)ae+ILpM5mLxoQ^*3G zV;c8s*ppAWbxiJ=b0?T>m)C!=L>qlNIPvX+87i1Cd3tLvRmhfy<_cR@)8B1CCqC$- zSLtiLrO!~#T~0HnA*yXG7=A;0ywj8rP^SAOok5VV0Gkm*&K-HubfGBa#q+nPyPNBK-HYrTM!ft;@T>CkS_nEKwyXYZM0Hx!1{L1Hbs8(KedU8vu}36D6WEoi8`#3G z*_!@qW*g&alF49fr9*NmxJr75b-2ey5d}9V{7n-$v+-+!KYRA+1)S%GAh=V=@hO)1 z&#&O&)#q38I2-awKyw^-YWNMMGyaoD^}6GX;wMe@hNL-|J3r+O{MEEwf9xe~$-vW| zJ72A&XPW>{dMu0OBb!w`9XaT*9l!1s@nG|MX2m($wYgCHUg#`4l72?3H``f*D3Uo@ zN~pnK+z=l+=BrE)MNav}tovLl(Z(lJK&$w(*iY}XOy9lAy8Ms?v~h~Ll5y8eM8x8U}9mgHxT{t~$6R5lW$jx*)6} zsbq{HlU6z_%@J)nw#Td%oA=fbh&rmUNL|<@thzVVsr_x1E57Zmk6F6S^e9vqGuHH?(jRgI45x zDc_X?HgZuh|C1Z!5Y4;}TW+`^RyG02JUClCHCwpQglB1V483rnOjkBwV4+OlZ!AO4 z{2G7f=uo4)qf)}nmL(o)N<-AmMRydUBY6n?fK3ntK|Gn8g7a-MQyKp>a5)n}Nz4t~ zz&2y2OC6}MJ}c)zKPOx!U&RCAphPtqispG22cTlKLBaXUUCj&Mp^91d(W}l`P(ztoF^^v!F-c(R)&+;{C-9NRR z@Qoh_u|KtQk9!;d+iAO{T<$9Vxl1u90Nb@#7l5DHwpFmVGtNdb{(7@9S%9C0fqK{Q zTL8W)EbajXMp4)$I*#CyiH5*Iijyir7VOum`;Doh^TzMk;dsiAb8Nk&%}fKM-{?0- z3Gcn%DDu-k|1IB(Qt5m>=$A6s@JkWFS6FJN$Q9^Ez9zJBpnE-yE=f(uVQh;z@ z8yE~2Q87R$pc89TKbbS};MuJogcugQ=97G=C^?XPf7j!AGK(&W-R?$i=8tQgtori& zyTlgUfcVlUKI=T6CApV{O0xCkH7EYSX|*{7A0p@Dj8!XR9c)8z@%A(nk+Wi)j1VT6 z$ACRra}puAkC!A>r`#y2lp5|k!!99MT6`lLB>^(!Pn;4d^lEJwJZn_SPGO2vH^qP< z-Lvv~*6X|QiV;{LFkNy>g&>Xfg7|=B{o_A!9ig>Bc2ezVgT%VqxVe7jeuL)>b9&c? zSEsHD7Rj*5%;D@Pj7g+>8U&U#_U{#~L)e20R~lGhXOd{dW^vu30D46Q&A%*IEEb#? zt(a^w_;D(px3W#C$MlF2ghCgLohpaYOYl+U$D#STd=Vw zAOCG-8`ganHh)(&?Kc+cf7Xlrzoxw=PQS9T|3JA4QPz<|Qh?`aO}6cHx%~0AHO(F39B6qB(#3g!$2YwLPni1IC`}>JSrO8xCmiv zHmRKK;+=f8wG6AHQCLjOC<~Hyw*ZY-Wuwb> zmucxM%GB_U(H=V&qT$WaNq!bH$o#&M9Rb$#i8y&+%Gmsb1$3BNX)o zr|(EF*uMKmMFjd80f*Z-vWK*e>ShHOo55i>s>g}@48T2+_TX>=HQ3cYk1Z};2qFY(-UO$Ks8EQrmt%{mQtue$xVL0MMz2UsR zeR!Eny)*h7u(;k^2svK1t`;-8eFk~Mv*LNNc6(wiT}Y-B>n-@j(c9gHO+pMUAH6fk z8$LwXb&j5nUWS9s9P)>St^z}#Ua}f!I)r&s)^~0f%Un-MuCB=soWGJ3*J^Te z(}1HHJ;3$PY65Z&wuZ)zjsgx2`tCydR#pc3hL(RgI)8EbZ1^Xa&)2=V9fP5~yne~U z{)~)*{ynQd7=B7vo!s$DVqrZ(<2zJxl(v+*lSINQVDplrlcFDuI43LWVFOq`0T_BWZ2c8bujOW+;Q1Eh`rGduG? zmc(p-_`SjLt2QVMh=30Q^Vy(686gR&Nq%yU&$(#q4aRzdNqhEY&V{I;#9QxLQ#${ zgGW0XCDTU}a6{#|(aCxj2|0i3y2KdkISwN>(vo&c?(k#Rr4t)0p=!8_T=9lb^qyfk ztkys(k_oC3>*ssN%|vcWV%Qr0PE^!3VxLmEYlw&P*Dy4tr)Pzqhd9*giga|DOgT2C zRB#EY*37&zI`vSh5E@pp#6zefM~@w755d~IY4%_J3f+8M&-Imi&0&R*z|wP6YvZIv zqD|$c_-1vMF)@yW`a|_6eXcM4*;svOlBFF-Inr+)eGgj@n(o?FhY3#5FI7`%e zcn@0n8G>r<0Rw2~2t=z1Dqy&4(lAsFyZk_4ns{X4zV$`FvV!o*0MW&m@gDVlR{(kV za1=|2D%@yht{2n1dPH#|fGtwGW>2N(S?Im32fb=HV}R<((jMN;-_gpprZIuhFZh`) z@gxd~>L*d+X^fcKbG#^@!LdE_&=zg-ljc6tIrnYcKD5eEIeG%Fcjr4A3>>;Kuhgdf z$F~wMY8Mr6MaE0#Ozy9s!{U2ZJRQG=1cY!=}U=aO} zK!WKHQIsUFZTYXsgM^~x3m5Advt6+jY|HYt^p(On2!b6c`Otm@Gg$F#(p8CCV?HBS zg`+M7xp3$KQ{Rwxxa;oj1qfZCI z?6Lu!UPN#B6&Gg~da%JnKEKxe#CRT*55?US`f&SJnCp_R$j=8Q$dTc0V5Kk=RwV;^ zQj^YEWZlRM#5Kq+6T3l#+`)AM1+YjZV4oQf${dVrSw48QGYKA+V!WP$dK%tC%$l$c zsN1XwV~j!7QDwAx;-g_J8H|iKWw9uk&|F<8I%q$fV)SP%-q&whdGHL>z7Ao>q4LZ( zX~O=TUDknIjzPeYXIrS2?9Ylt`IbxgP5b_mp~~i)bmtAT6Iz3f0bUbEZoc5RM(lhY zD@^iT+CFX>BYe#8AM;h0*}}DS1(!&UbiBAb1~CxUS&!`~p~;+>zh(H4=^*q&!tq2R z-`rx|c2cWw>yShUmtow}!coyIywREEss-i81I}LLsD!v;K%UUm5k^#z`ZxxZi|?VR zz5y`PzbDY&1|V0?o=;7biXT8^vf15eZFB6)Ll;(&cRBMK6yC#GX4{hm)+s7&>Xsyl zmN>E)ulJFfM>_i==dt!%F6Fg}Cr68QQyyy7~eqrx&KRtlyXCcj@?aGZaXKVHAV)~9%B z*z&)jGwNmLfjZ!ExdkFLs()m}3p?9c{o6;{@qeYB6)zgvIQ;}}m?oBfWg(@T zNGyI0AwEzjXAfDAjE2@>eMX>O-!L0BUVH)u^`%~6-jNUWq&#qxi08I?cll}ZU6g5e zQ+3l9#4MMh8e0(?79p^@SeXqHMX1Mwb&4{P0*((e++i{pksNQt5X%hfA>d)12wD*X z%zxj$yTUX91mXw-5BJX zMmA&m*>&37S8_p@hS*F@c5>(`;x88D)eBEm_stv3cv6`)baEB07p^NdUI(H!c>s8p zkU%`i7l}Os9X;dr8SyuxJw5)x0YX`}{lo5f}+OSZwW7qvk{1SvN{dRJ2=S`{}@ z#&9ogF>Cb6tKJP6*&N1+?VffS!deYv$qu@xcx{<)6*Zc24Cf}gtwxnZ8#?i+u^SXJ zk*;o_YoOnTqSJf{LcDe0Ru{ci`Tcov=D3HnZXm)T4#_if9pX(ZYwpB2zwRLUo_cYs z_)#WUmMc42s%zplNw&C$k3Q2;Yi%8q=x{#XW2oSeX!VspK>vJQGbsKo7-5Cu-ud#k zIh`E|CYW+y+@As0ALexa%fs~#8C3bZ0B|T2He+tZLK*SZ&uvEP;qzCK4#=-T8%y|>2wXwG9f3(1YeW2SjhZxyem_wlyO(zuQOR15yG116Uh<%5^t~zYy*cH zKS+gR5WRB~!r}J|722o4Aif~*rr~EJcE5!U6Ku|Rj`^SRIBotFf_?v0?TZPlSCFCq zXC%VL>K*H)Re@WEuU)GL40q2_3w>I|l}0Xe7f$R&`EWX$gRB3RK_N-9LqR+Jx+VrP8NDDTCO{lYs+5)_)kJ($Ih zdC{WWOJla2lbzZe7$qq~m!XYroEeIiGuq#Bm)3LGP;-Z*#R`H7@q5#Q4czx0UKm0N zdb6yps6u~lEA$e;q9V&HR1Nj+Q=)}j^MC4di;bwjq^exi+7NRS((Izt>NTK@-r4>n zdL=$^st~UP=A|XenxK4LviHc|%$SDdvHzAGE0DeB(o=*d#ij^7_=x|Yf)70iC4>gw z!8JxH=tk+M$SzYdjn#BuL@g*quBFgd6Oa{`)iaB1o-w7?S*aI5*B@^p8%h-|wIqV3 z$d1uXoTdL&Mzw=ZDJ~EzEAc*@w$2sRuek$ zT0^c5$D9+pB<90W#rX;#GON?lLn^U*7MZE(*xA|K_9k9dfGG`lQxh60%+_uL8uZvQ zOovkt@+l}E8j91d%d0E;O$?K~HGK`))U<>;qdhcCA8MlU;L`M;sb~ycWt#9!0=?7F zx6N)B*b0oX0xFDlVbcXH@aEzQQ0H9Js8FiFbs|aPeIAR#=#l$*oU$ygodhojx@_GqQycEa{$v}N6upR`D>B!Oih`q-PzOE)0=@g4GK&b4iW@P*F%vGjywxMHi8ykQYV zIfGE0wSIp3Y6b=@Oy{%cGKIQNdbZ3}WSj9wB$vrZWE*gCnU0j#um^^8k)G{pZ|Iwc ziv>KDr;RbnkNC!n(8C3vy=csb7*Bd|m53JhM-x*T5^^=x5$0<&B>Ki+9QI%y}Ry zVHBNe^FM5~g4Wk{QuV?S7vHv|_J(l;T}WQ`QyRMz1hqNPRyh>qn9=yWKvRE4e>_Qd z|HWG*_JbNdjBA|Ph#gJ!no*H8MD_~0GFL-E5noZVYKP^=7MEM5qH3_oYc$&xDOnX< zm;H-{9w*6hT|3Cin5`AwO37D!i*9mWP`4DrSB~$!;Pb0UuC;_0OvNi*gmI8qSXq(XbLPIBq5`~+&S#OFDSWBR zwq$6VW-8*vg14@}X4_e?A%}lKEovYR`Df$s|I~eQ`VK&2WuSWAzl?4EB%$XWStHrc z4=e(wQ_OyaEi|HA#90R?IzafNfHl;vlqWRn*Tt>hZ2?sYAeuusX=@OB{S|+~eoQ`C zH*WHMa`*+=1QaG{kc*m5lIt*JxO8%ZTY3CRc5F1{zdrVypbbKp(d0l99M^~xqdZ1Q z%ubx~#_+U+Li;gI8VX@>LaN1#OPWfyrcL+2gMiEX{4lfNZGXdhUU=CbiXL>^^5GDU zu|iKkRblLETkvpZxb+EXFgMgL0M*dpxh*CZjtvaXH(-~2{cja=O54|9Dqvw#@W0V5 z_|wHSuCxl&EkNW+_yjXz$DorBB2PprH5`C}I)L-J3aK2#J)0V|oVrcPCE&NZ3X&~C zvaN^b-Zw07^me|(CANGDw%tmiZ;N?kbhJ8|y4ZsG zYj?C0-MEG#iJI=*YNXcR1^_eEj}s~%Y=S6s*l0H!uixy3s&S=S08qT;0k0%dl>#dK zl=YG+^klJ<=O#7+DQu#PVR5o|9&1Qut*D26SlI~#dz}K1Qhm2Qqqw^Z%l9Dw$Jysg1rhC; z5={{OGFJ)LQpX3ry@2?Hwy9_f!cja_RCS$cvR_uxJh$Cr=&7H>PpNkE!>cu(@tluf z2sf*rKBCwOg8IVELYs1kv}B@ia(~vjmU=Xryv0G{^1A;s8Q><7m@Z&AkwQ(dL zY3T;B(D^Gy$pK7lviW5ZkExH~C~n10ZR$@#>u6;L15&QC;gpdHN@=EL<-(i-d69R} z0kNz2cMf9`YU;>Yor3woY~dR>k;VgNaZ8XzoOxEgxf9cIZOZzH+arv%ur02OcqTzA zp|OIb{G^Xa2#7)jXTAKy#;-f_6<+8uVO9kyyrNur97cJDGGZw`a=VbO=8NPT>;X?m zlkg}z1cG;w=sVNba)XcumYAS0^<={*1lzwWHJIO8|K6uSxq!s&2IeYkV6LM0$9n0% zkg=kZgT9lo>7O38jbkR^d;X<0oSXQJ5_ScGTpHp1i7gF(UNuAdC+p#p=Vh93ljIai>POsJDH>_}zOzy?X-t7t=~!uHjn zX%ghhT^Xo$^Q4bHdUgYd=6YQNp@e+zY#C=6C;9pkrd=eDd}t0Z1|U!h!y>SAM~4#T zIC2lGd?r&OS-)KNX%558C>FUPnO7o9+Q|uz)OhOY3d~Xt9kf=snW>4Gp?#tJn;J8m zhTBmPa9>t|wIt&|?#(~y$|yPkx9JZcI!Qss5?E4t=P#)gP&Vu~e$T9GL<}(0pyEwY z!1aJag(c-^evF^hSYE+NRE}jGcvI&|6oh*E{3Kskm(EYImjmtMV!Dye79-%=<+~1Y zv<c~Ae;%E4XyDB0G?b7+~v zw(tpIyACesS?aL`K5`zcDG3kHXTT_vG@ip9rtaame+e} z{Q9tna-;L7Lq4w$`;QU6kN0abKWZOD%$maNn=+1klT<2>K6_&u$;vrPmf6ZA>h&9& zmLfWBI6*zVAC(sshc4!4C`Y!Y=^a(JCm_cY+;CBrN674qEZXfHj#Z4RXpgW`DrW8R?r4rWc>`B9-iH=DJ0$yrt~NG7`a-Mj=@sqlnT2ZC`;DK z4{NRjz5R?*_rt8QZ)MFVwvwr{~sn$zOYXD|U0GFv#K_R}2McGnFA)Qv1G z@aRJH3t9XTRH>rG?JiBy1AQ)Q;VGE$*$kYJhcvkb+lk47)p}K10ZP$$!A~a%ciVKe z<>0hisqwOJl1)%la~fv%hFzDaO<2s3JGRRmN`5NEd41 zYI(~8pHVLYnF0~iAP258d#2fw=W}eW#64M#6d?pcE-+^f9|bwuhJaf1m0iu3L6j1h z!<1M8o~wKLxufZgxtQp-8oFFvxce+AP;+*=dWq}f9`jbsAT7yt&YShM5f;;6>)Hx0 zgZ8`CU*K7LKvKb$XM#FWHj&%c$t+!6U;5O4KKn6uvR^ITVDS$3&Vz33|-wWs# zMnOZIeyU18JZ?Em+-hqO)3lPrH=~ zBx>E7(-zWUNy1lev_1y+mqZ|AFP48_8fYHeIRbPGb&6DfgL2jtC!1r3Xb zqnjH&Y#C@m7>>DGPU2_`6p8`uuyy+i-*QA0?8h09U+<2JPp%%_d9!C1#aivN13mMI zJDzbqa%a@U@W}PuQgZ=s4%1I&K%Mg63qT}?j!(4MI7qM@yNo7o@jpXZ@aa`-H3v4T$Y`i8(ylB4c+2jav!X2@@Z^{W3^1 z6X19Kx4wm>Yg$_c5Za?c{&!ZMisq&^`c6RO#y{PaO4^RIDu_Ib-wDO!yIB|rw#Oti zUI|bHJ_#9$4?|4Ve^(I426t09!_kZ+#8NleN-745Sgd}V`oW!uC61Zme;jRfM2Zq2f8Q+|jm}&D-UTpofeDOGa~H5k*s>#S$e`Va27T$yAnn!HP%g&(Qzv zRMvTXuA;25^(p0tHk3SosG>mPIx}}@rinu+uRgo;fD@ohGt&fZn~3#XiIcoNL0`a> zsh$}}t&A~l#D;on$S;bJC^Ya0EdJ@m0m|ExDox@Pb&1*vh(m0Q(S$wpod{}-GCK3) z9K|bZ2g#nN>Zyy`eT~Gh47j#rS++LT_F}#ESbc&mw!zO*g@%{}x>buU%KF10%<<=t zPf8+SWcHHPnI>{G6!~$C<;AV$N(Ss>h>q5L>H6feBLYVZkubMkgczk4xQZ{0i0!`% zd@Jw~j+IBe>A=m5(qzJhEtdLVu*Y67Sh9l`xE*Y4d~ctKh-vK~*o)82CN_f|DqGV& zuokrbhFh70DwsaIdLuQj!j_JaOnkzsWtrk%wm784R7klW zV6jo5Ne}HQ4`&c4IQ;itJKjXq_+@)Ypx%`FZe{I1IFyiHjgF7o=iXaDtzJ9ms*mRzj*`#ZUazhZ$0cp$q}i(yJH zd!<3|PfNy3tF3sEUZ?H#T0@)ZNaUEZwRl4BOqt?5nKV?C8Gko?5Y;o}_2c<*2 zQG*=XeCO=scPUqX1u>2|5~nw||HQ1*b0FK2X;%+aJW`)(i58RJ-PsK#8dE=^zJn{>*)I`I+{j4%@$drxM8TIZqZT?OEgk4@$&K_BD3*g2XB3y;3eY6{L9~TmEE*{q`?7^^CK_` z;Qb?d7PPYcW1l}*9!Qb-msVO8%_+KB13yfI=9gugq_@wQ-)LRO3U5(~i;+-pO|}*F_P_yn(&SOwS1($AiNvy3J2#y99k6aJ(fMoYG7l64X{7nriLb8+4>j zIR>=t9TuKX7NQ!`zJyavdtXL0QHBbFvZ_n?EKkSLG$^Hysa;J&cv31Mt->l7yGqQc z+9O6IC{(#f*H@3p5l6kKERr(BhM$)vZJFnaGsHW=E7_EhTBm0 zWmiE1UZX5!SNER zOrpsaFOQHb&KH|^{$nbQkR(rx=8yGxr?82GWtN#iv>By!_|kO}W6y%I+*BT=!;+sg zDvP;Czrffq0`DFJjVS-lDY@_F{Okh^t|uUBr}@X=k}-BNv;DVQx{`ywjiZUJgY_TZ z*P;|P|Ha38FY?oD6sZ>6*GSHrQZVYE`jLX)QvhW^DBip4!a*a|XgYGA__%{`t?Ngs zM|}PJB!+R~LPf*O(2d=3;@5=N&A74e$J0GtH;4*kodS8C3hR0JX`=M%P4JUwiMMSP z`PfFI?TE(m&ClLN{vdzQ(_uz!DV1MMg`oOWaN0p(E}>HXJVDdZDKMXL33S$g>QU=j znq+%yl@g|@c7_9S(H;^_ux4xQ`5m^Le3@-E9u!v*@GvF3-@w7)&7aGQ#@;d5g~*hY|&H5Azx zev%ZueDpS_Aw`~G4r0cX;TetZetmM*bmuOE5^OwHaMG5qSyS#(sDdtC;9J^}jp`6Xzp0`1 z#ig{W;!$e?)+7+Mg~}ffdWSjn@!mivbs6&^w%1__zoCJV0dg_em#> z4p(J2DyUQ7%b+Q=U4Ixk8zCOek=Rrr}|Fh+e`*n(7UQvWQAsLc~_ zJlCPP2UDEFHxS6g{cbNngZG)G1n4Z`wj!QsGTQxx*(l|J_!PXWR_m!QBD}cI4c|T* zlg#5CtTcgYZI=2+U(kx~UmuWnoe_*MfYD73jPC!D^TF~b_@uaLi!6Znj;>KDHYsQ> zRZ-YLslraFn#Me>=Vd;pXU6i`{o1^R)^4wv$yF-hT{Ug#akuC3?3-~0*h)D5bVz#U z@>{2^iRr!1>+3TdKN1A&YF4buQteseDKpI~LeO3L;V|2beV^5_9H^nV%M(_U)Ud>!A%MJZfw=Cg9i7#afb19S1FYoAf^Z<=BLhhIKyZ^4N1B`5r0@p=jAOaM*^knH zH<+4iPEQi@t$MyZ9)-OLUoqB011zBoIqoR}-qt=sm43qk%xE|1t0R-x52`J;z>}SX zV_9SQ72Mgd4vdDNuv|>Sf54YgYU{8j>9eW6Iil{r?x72rDBPRcU)kG_!&@0) z!^(EU5`3y8DljklEq(Z^2OPz~LQQM-s7VpFD55{0XtIjD^Xk zh{WYzW7~&N#mr?=va3zdX{&8aXsfUFKT*fCK#C7F2g_*CJR4Y$wMw!ER>b|15~SL$ ze?mG9zHEL2dmzlw+udUNeOWa(BQz&2<4y`)U|-QD8v6EGM<{Fo)twVvzkdgmw)zU@ zuhCXIOo-nPj5Z0NulZbkK^;W3Wk&d}XbQ6ivl4 zXN>jzC~eRe`FxQou;k4lm53m2IXO5y3fL-B5uu4+8S7gidaq3{M(xDGqv?FyC$`XR zT4DPZpJy_{HMltrbddCAeoWLHmr?*GT@AMwr+kSA4}We#ni4z9UNJ|0@5k2uHB77W z+G?gFOSrjI7+?ugmmyCdeW)LV_@F&V75OEuK-W>r86NX_myKwIkt`OTC|F`MYDA-G z*P=)YN2pLN{9B`gXYkJb;6Yo*eCFNV8f%{($^u$@zY`=K{Vcwo3Nm68nHMH`Xl-eW zvL=YG{&&WQv?7rdmebJ7`Ji(ZmO%Z=<@z;jh5Hu2$TLUz3Choyg25=WJX@7hBg7K< z;5W&YSZ!%E_3=FszjHhZ>7+Z=D-xTvn}T3e<`A>7nUv*X4)tr612A-_hF0%BJv(BHDLKs`IBE79cd|g>+D8`ltG6jucn&6ldlDd)J=MI0^Jbw}CFUiAPr}}|vH{2l1g_%kg!x5lPuYY?N z+;Iq%Hvm2WO94;E|8z3`l!m?I#=isA#Dgb4p_L@@q4^@A5BiZh?L!+%hmy)*Yljlk z7CZY96w0jhIv$H6%XoeIHtME{h)hCjGVv?bvx_73>DA-q)0A*j?oPNIf=BDRYeC^^ z7TxV>9PBs2<1a&p=JbHxSlQ+ayQZp5bs_x_b7ECi7q1I&p2=@ed~m@8P53NCU;0*p zos8W^V$0cDu^ETU#08ASgF&(AmBxK%>oIG$C-Gp(oNrufRd%ZeX7@Yyl8HE{`(xP( zsceRbq#hl`Ivxf)X>5$@r(uE4D6887_#kt!IWNbVN3O`sNB(B6{8X8Vag@c1YF|bO_-s1`83)ZT$$7In3_Qcv55e-exzd^P=D*&TI!Y*(?2uQ= z=1Z2vr-xcH{f+LjIXL$z0NB)M1g?Mn-~M-)@Gpe7<^PZH_7)c*~+&DccW*~;m=zBTZzTFw?obo*1gCQ515VOAWG zXEz;H45kOMTML8$ekqK|QJoeC!X>P4AOhI>+iv|)!_+uH=^%-%IKtJ%kLLBcN=}KR zO=`YVVF2QagFmJKEWTMGQL-W{i$iR^SkvR=)SN~*L?*eacT8v5;_Lqx)u>oiCZ$Hz#hZ|nlVHAG{I!63-=%t*m0|U%afZK zlCqt~xSV!_7_S4Z8l(9Z)PXe>Z~!xbbd~-yq#||IFbfm@Y)!3FihnnaR;>duiN>E*Y?4?Kx z32HSe9PU6hA*ZNz{JCC?zHFn|J~$($vS|#HFyrk4wGbZ*e{v>C4d-gkOx?cLayRCn z@^5zlZTE}$YJykQi8z|~uI(5O_}EW0+LpEBA|hr-VL7+M!YlPRRr zA5#oM1sxX8qANcoD=n3l!>Ol1g_n0zlYhJi&9fK@_THy;r9#cwEz`EdrSfp#=Pmh2+|+~!Ce?=-U%DG9_Ih)*Kgq>#oMPxuG>O|q#x{@Su3*x{uoOS=ph$X zyLKny7(}~HuE(kSoGIc~D$(3o{dsfG%(ya{#K9{dZ7m&MYZ$ z*lVO5!yR65t!z-On4+tWH8M}D$Sq9b>&}npsK*hIH(Mlv&ljCN-t%nta9fben_e2{ zOn*gS2#V$h;lOj92&6&$bH4fyVTAb)2&_R#QxQc1^_>$6mbij26#26Vs{tlD_$5Yr z1=3pib}fv_qKFik5Q-=uRmo&$pY4&%>t>mfs8+LKzs|Q`O1n9E>epv4Ptyul z-K{xYWihh?==uM@f{nd_d<48}KLT8;eBn~8o>Ta}s zm64Zu!q6)PI+%uLNe;6*hzM9}X##QWBvt@Y2`E5d>i8#xiGbKFZutVX<~e3cddzq@ z*Z{Ya&oRpFP@ev|KR3i(RqG?OP!=!>!D@!5yf*4(E(!Z~_W=Ae$5lD6D$<%gOD8_` zV^i`KtB*oLsNAzPXlKTdWDs?@o>OQdU+kmN>l+D!Y@*2x;!pOsK0C)6LbyntAP&*i zOPoMNubcgt(~pk!6H<&6W0H?pkd50Zf#{xhd#ANbBx9++(YzO0QN16h{ZG}4FX%bp&H5ad%5l~Mvo zB5k8Yzl#^)jB95)lw*uvB{^sNHM%oC4Z-{}e4iX-ifu&MXKFUdew%J|msfg7%|OQX z5X^m%jd!y%jJ^(B8XD(aMe3BIUV>f}5BB0Y*C$hY4S`U5prttJWusDLYM7 z*ry;$MFHl2E@f1CoQGtdyfRH#}52L?7 z&CgsE20mLNA@R*C9gp@r)|fRlGyXB#iy;ov`w9X$Kj^wphU9hBH}RI@ILYH!yXouo z2CfGY^Tj}jmLeL3Wx^?~WKH2WH%&&;=pB-Z)dnkpe6-+ZhxE zT7G4{H^wndDKQNRvCozCTOS2nSu%RZ(k5|n#dAL_1VJhUT8vMel3_>Ap{Sd~X!C~3 zUFoaz_!_FBTx7xS+iqgCm)3Ehl-fGvLlISRN^;Ti{7vxCv|wsNTPG`F=Gq1Q&s#7= z+RE+X9*-4WB=iconvc$PF7eWDVzNH5xl8U3N?3;@o~l{ypxe}mn-76xjuC_0ZF}yL z?$sM)zP*NW)2cG!Nc=QTo6hBHzg(F*JvNtgSulsaDOJ7|d?DnfFYoXILtEN%UF;^! zpSf@&V&B}_gx0DN=%Nz!uh86i<~5aVu5_>($exaFnwSo2nyh)P`NB({+pB8`_7j)w zNt2ul!fT^@CGr3(pU0l*J!rpzu351sgb(#+SEUImZ(D>X_v4Q$`Q`4K$&bMZ z)5Ge*{G!ZckDBQx*7q#Sy7Z!lekK|=USNN$zWj8x)#TXayJw>IA%KivnBHya+J1wf z`IMHIjmMCtP*X*Pjg>eEEA8GeI!q5X&U!uE6lM_oK?`sYnVrWEz9Qsp8Baic!69UO zV3(Fy*DJyub2y@alq|}DCA0$>f_$Q(wnqUqiPkwfrF>e@maZW>>2Ml@)TD}eQY5{4 zTZE-U0^j`xGw3Vggd4hhD>Mdsp0xa{lm0Q;l(R9&1)yhOrY=CmX%sY7n)4M74l+l| zKQc6Sf?RHjyub!b#teN%9gK#gG=^ZWBK2Y#rNjWTfxK2Z%~R6V++aQIR>Dt{%5S!D z$dQ{)-!GnIhNK+|D-hGbtlYdNNS~5ohzH>B@bL4Iox@43P!H7*DUzS)UfIA{`p1!H z9D$3VGBPd0a4SK0I9Y-OU{P$k%w({E;(hhk8?APaGS(0KD2>`4S4_3Etu&7GT5c-& zhTGbs}Ven#`FQb$fD?$V0L!J$P$L_Ba#(zyRK==5CgG z?9b4Fsbu@3DB4qZ#Yqh>39FsubXHw*D--w5^0)^H>0b#LnSvWzjz!J#GmYan6jH9* z7kM${*dS=(c2enEtRr=|QlOcko62N6Zu4PoI?fiQ8Hfe*;jH0YVl&*{r$?*B7d!Qw zTEa?r6of_5=+pV4LL#?i({@O2w!@@;p7m6yVQ2R6hsU1TDw$PJ(ah%{rxY6YTI8l~ z5)TB=@IP&l-hz6HYqU$g#lmjE3}T35;S^ScsL)lsZ+U7}5i3en?i&fV`a|)p)038# zeBzu6qgX4b{gO!9)m~MwPPA#u5?drO<|U4@M`l1y*e3o8&jbOZR0f@R4E`u^{w^`o zZcDN4@7wpvmGjQ_*Q7fb@*wrE!4D8%;{C(m$G_*aiT^yS?JdIq9Q;7cU`gx8oQ6w0tO@XbF2msWaqD?^qLloa3#MZ@yDNW6dsGl;HfEqkkxP!>Ua(td^6HLP_AIk!z6>^HaWAOsNp6!$RMUU$n z*$hd*c zO8Cl>+p%RK4jNoIrc{fx_uxIc+iZKEeh}L8+X?Q75t!gn1uKM;Os-~n+ipyp#bjr9 zeOd}0t44>}HwLAHBs%sKh=;!9FVUabjDe7Vi_|{|>e|-n-<|S2&JN|Dmoq8Rt#)DY z&wUmiRNyft=emfCqO?GVO@Reqk4ENBcTAM^=>ZMTMYzXqv-H>PtK_7ig17LG$NW*v zlV2`m<9VYDu76&b@2HZ!3p8+Mwj-V?9S=4L~POTrh4z9L!n z9P4Ds4DmEF1ggW3P&1b67;5*`M!8d~HAkR7{3ev;PlT_WjL ziE=kqKZ~37uG)p&I4!jIjBi~ zGVUO87xw}&;#eOQocFlCcKSV9(tYh6Fyq-_mT6e}hDH+LSRP$G5rN{eU9M``EY{%i zyAFB)u2m>OW2u14D(-_dbA}zJOH4$_gsQ5Yp>r@ei?72rN=Gl~LbaY#`!M zp}`UbDH4+bTA}wK!{aI%_^kYB&3Vyao&5AHhk7(B!KJPyW3nJ^NRm3D z$;gq#CX-qd<#my2odO0Yxb+**nNKFe)0CW{y_};OsG4*9Yk{{x3ZLF!57tw<8|PRX zlIRl+OYfY;PzQ z7u{KlTCp=o9|t>Qv&@0XSLLuMOXcPYIiY|gpPQ;m$ka@;T5uXmf>8Yem13dAtRP(dstN z3CD2CK@M3ycSdnhzGwqx>eT%YERy18UXBn5{6ByGh_*o|Nw@bQ3bY!1qRPtYJk<9ZK>@+h@q0&2DNDD_%W078?|e@Z%9zmD zjBeuoD}}8^&!Yk-20PlI>7$mmdo-6WvGP~PRQ%*PXG(HZncvuiZKYZ&7qyP28mD`l zTzQFJ=`jb1`725j$(AQBwqI&W4PKAe>o(}V7PmhDGGv1;p zPW*6#S9TJ*U4jjw18izN+!#z5WmIG5t=9^H5bW;A zH3`ihDAqxWj_6I*L0`QBR?D9mNY~1`Oq@iFb!oZRFY@y8V=5j3t}xpg>%(!3>dEw z=|6A))iCqYQIq7ob`@rQ{q{)+)sdD$xTzu*+^Dw$@<^kx-xfxJ0cG{S5$TjK!w@N*CB>K^af6uo3p)(!O9PzwyHRDsF>s z2l9tLG0Gs8-Q$LOpi2fCazYY&833^59PUOcaZlk7Ll06nUs4Ip@E0qxq7S~m-l!n( ziZIBqpFVwH|94zrAeQ)lcHzYTRmuG!A6BS&dMVzaePpdNn^$5y`ziGbQK^VandcfH zm9#(mnYovv*0<7Z6tSHLGefzf#&XD~wjR%o5A|~#&V{8>XAQlDJyJ=I)9bQ76Xgj~# zDgEU7zB*I(yt{prJZh{V9#Qss{0WKssa~qp;F=tT4QyVGI-=E2E+bB{P3K${$g@oD za*$z1y0e5|f^xY@r*|hVfMI-sD^H->ofaQ&nklRN=LH}Zl%m9T%4g!5f z{pdp~Aq)`G9YicvT*>MIPiNNPB+Q@e1{sxKX*SBilmMo)Im^Iq$F109 zc{}_(nGZ%_U3tkISqI80@6z}>b0~c!1bDE6Kce+Le-n(&tJ%)VT`h6z5ZviWBZ{i^ z=B5&=GoZ>ruN~Mh<-;CLW8z5I!QkNO>*sqx;!ZvREEYioHlz#J9C`%^tQ9qC0}N}V z0T@GJIjhLq`H8WO9H)DP-bZ!6PWl;mOi(SrDSPKKFT$oJa#-IenX9hv_$#dAn=SsJ0 zN08;^!uO%?{;mrt^akBbymo$s-1n(bq7bJ9lPEMt2P+e&!O{*wu2H9AwHzNObc)zw z#dO3?hv$o60zHaT*Rz$>Yed0Sn*?cyf?~lO3p;eG&nm$W<;B85G(a%E@94={$Or!+ z?1}8HLB3JDTqUyO!M?{IlxsX7A(E|M5Y9Vs9A2WI8(z;TzfJW$IckfY&KRarB|k&& zOiaH&B`$$Xy<{#%rZ+O3rY%TMCisj3H;9^rq#v^mj_2iIa7`H;}(~dkrXn-1glgW7Wcf00B6S8<&G!g zL{CdbR@8CiiH=&(<&EHvh5QgNAEvJFS8g3o1Wb`@+h+#aziGRJ33q+E8PFxa1oGzJ ziJHjVK(t4qO_q<8l6I_`_7tQXg2pk-8e-&oY5kHPyhotYI38n{`hPfk z=kH42ZQHkE+qP}nwv&pTij7JtnVGRuv0brUv2EM7d9%(r?Y6tuUHf}?yFbkN2h4Xq z<9SCPy?@5g2Ekf9sKxMO>bA5tKW!mX1t8*ax8sE{SWY`Q!U|XLr@W4vn=#>dc0PXRhN~vVF<+=d@yP zF7~?b<&IE~?`4hF>)a9KMGPjJhGZeqmEo_~-pB?#K!Xrx#e0~!j{;KB+IOMd?#g@@ zw+IIXsWy*&*3FrQiVB8vKv2l3hA$c^l_1q{3I^9RE7*=WT!zKDZgFfBEDq)Me%)AJ zyK-F1IF{m{qP$Q<)EfGpqye+Lv{7%Bnp~{k)lh2AFZWg%eiWtm$u|-|(Q;UEsU~nI zx6<@8>nZwOf-IEk!Wtm1H2g=cQ0O`l*qjsIZwxQjpnwc8bqZ zf0IODNt4Du?eFu@C>FLH!-ORuWz7kC0+r?cymw*1Gf%N}fEm<^TnOgj1ZGbzC&XpM zIrTUi#JZ3cS+7w`kBTK*%1JJsV#Nqrz?JZZ3<0)T!$O|FT>ST}6$pwv3WY0#%)+MzSK5W8zNMy<+rY2%<=XGRayM-mexx}Hr2P0;Pjuen(r41=jzKZM}g6W z4{;i80--i@N%|7k$glAL*Y3G%p0^Ooo^4T0pr}QMfpr<5wp(sH9xC_Wiq0ha83N2tJ1G3BC|8}p>!3x=4F_t??t%u2>nlqWKr>LtZXmFF4BGUokEqQNYG3!vH#6Zl$<1F?@Vu5DT z+QIWsL(SpYhGK0ry$jG(=?F880!K#~HF&3*&}B$FeZScfxV^r8_)#;?LgspGzh#5zX|G?{;PwaEMJ-7EkfLu-efS&T>i+_{P|@zAwE)*tEVTzteG!xbpnGZgLB9{p5#0bYJa-ZmQb~R_l2# zeoRd0f`Pb0^Yb!lj+5K7rGM?ma@h5N{cy?#yhxna!bc!v@u)R%xvX)OJ5N+yw%aOB zX59vbYP~%&lf4IP5*1S4&12+B)tE=?IRxYz`N#O!!>(LS@45WWcp&65MY;txuec&n zyoJ|cfQF!Mr3c@5{^llQxn{ZO=9-YcX zV+eTSH90d$^N|a=$6eu&`(HX9%GD0hFdR zAs5R+s4K7;9j0EOm$E3PD`v7MbcS2eVW=|9U$6Y?M>PF=Me4kz8JZSPn0)^-*b_!X#D~g$)Ua{ zhhGEP2_0k;u9Mga9OO7WrP6{t3MD+1YzAIuXnlz@$9zD1seklIsR{`uHSVY#v-=Kk z9+Wwj;(Dn;%rt~pcw7{b4Nj}bq;b*6M9JtT>gDK*L5xU5gq#$raxyp;6-J@ zsbBoccL_*)%r(Prz=a4)lMuFrVry;sJ4+W9h3G;#{19VL?siOFX=_i!`ju<>VJUG`w=s4)1!Hv`a)sV(q$X$F1LUSxjr1M*AbEEkh!zC z7v;-Q+e3|2d?tI`Fsxb_p*knkigAv)nL3-E5+$Q^Ns4UTyw9Wwymjjpw?g}VaXpJm zarbDR+rN@hf&6`N_(u2+FxMx^@Oky3x zVNhqUmL6XbbE8yVwcNZ#(K&3bih`LAJwCB<5nQ;B$(2#r6_QLSKMT~L#N(9uGZc{Y z$Kq<4N;kJFspWFecoq5{yFcK>b2hc1k3|K6Q4ri_2&s^}5gYtL#ECqa?9)vr`CifB zW)?R}{EPpZ61FK&DC;IiP8Jsy2R&|)1wkMR8=k=0tY;oyN6i3-*{m^mrIj$PIT2T}AE|~>)58jxp3J~dy z$p*k!StllRihO~tyXQi`l%z&bsdPYO`9y`GM@bqar#E7WXC+kun(JcaiShwfhm5f( z&QN?PMifkbAH?DW0cno@%?kZznnKw2>!F(b4=i~9h|yIUl?7u#7dWNN>qAZ;)}B{_FXf(JNEG zGOnYBIe=)D?xR_2-Rh4o9NHcW@(9;r*kEZ8jr?j#AG{a6mQT^4T_W>j?DXkrT>lDX|Rpr);$U3B_54zLR-t9OREm1T($^=}SzCDrm2J$8PCNO(-~ z>*_gg8k4VDOAf1(pr5`HzKY^qi2n(93gE2yQ{b_O_Fwf9`^u$1YG2YZ_Lp=_@gGm+ z|DHf-IU76vGXdqdhOOG_zp{YbNpZh%@kOBv+E3VbRPbgv>pA=iz;VM_Xpl=1iqu1G z_0*-+cgJ%m^DZ%tiv3bkDQueB3Wzs{DVx5(q{;@gyNG|cXid3?=E(^l)bH>BH7Ii;j($%E3DU;v zJ6I#~_^j!yZ|+wTxpJwu1vX7H1wvxK^hz*7>8w4#&;DlaL#{d$_SBkV)(7i#`hH2w zV2YiLGokv#`dAuG5pTl1s63=1tCbU;D2W}fz(f%_jE#jsN&jKA;Vdvd17j(IaN8(N zVQ0M@3ttN(osv^@@pQv>*U?%Ge=c&W)l8)h!(T?O<37L{Tx_f-CEk&WPg8o^=cwjF zXO`%wFp!wiR;h`NDp&-GJ*P5WM*y_HXFu{?SA0HX&g`OkO00Bx`U{w}PNYrK+w#d0 zy7oZVA54bv{e_d}Im*SQxKzVhmxA`wkcK;iF%{^Z<-MYu0)POWZA-jub`f87QHr;Ue>P0!)9#4j=1VV(HeH|Q>UZe(PJ_4IO5D` z#<1JGhGZIF*08l*Z~{bbvcPkSVAwHTEzLgLPt{)HnBl%iT(s(@LjOeea0Sy%;FlVH zUX@CALLxUZbNIr!@N6Q>D0Ww^c36R-)AKcrr zMH4`ril#%`0p+$8L=;`p6f5!zKhb&%a5oSbl8$856(bYrdO;fM{meISXxG?Waffhx zA|iI|4A>Ug2d6j{I%oyOPZ1)pO8L_3_dp}sO|%FyCvkQ}Qk&>6p3c_#k@pD;s|KLG z`$r&ncSKQ{*Fv7YChb};BxTGzB*>q=RK%J4&@6|lk#%t_O1^;V>N5x`$Ug}T?xIF z=aWxKX4a%nV9#rbF8e2oLP&~5VV%4T*7%iwB*q}Y^9Mgu#nY4l*LT=bVvxz?5ayF( zvvT$Bf-?hA&5+(rCbuj*xm=r$X$WmU|IMa@yn4aU^+n0c`NBXj{>M`FpD+;rY|_!x z^Y{WmtWzoQ9&^(aPKkaalcl$YQ-GyjA(idzxACrFV914*r=l7b=7nKP0EG?o=zlf1 zgL>KtYGpTGZa7Ov61+Zh__FQn-SDwlr*qQK>CmO7x;!*Z3cmVoay_4Rewl5jLfYU_ zf>u@N2Bo9doN#z1P}nys>t9gm*zKa=VoE`F+domK%`g#w6>%4R-iB7SWh|sH z1n{+`)@!6lChzdCxSP{wv4KC981vD(HIhEG?f#a^tQIVZK=9SblIz zxkov}ERSKOo+!_X$tpmPtqvhEx(>7{b>E1z)JXW%kHH^Zi3zCjP>^h~Lv6(&Pr!06 z9l+rwyY^hC@TpOfM0!9odp}cV$;2FxH19*s(N}?kurI*3v`w)YUU$^p`GyD!6ocm@ za}!tJ&O&eQh5yNSIYHy8i%rn`O-$;qMkYN!X#&h-tOSmIOc^DY-8OD&t*<)$>Tl&tQnmhyQ z6~O*UI{>`*!KS03E+0O9vEr~cW)!sd*$o7YEtMF*9xL1DGIldhV+J`gf+i_= z>HD+!QABvp)`+c?RJXX2-7%~Od1Ezk9n2ASwLoW` zF_5us=>Zhgk`qK6a9UVzz{Vq*etldn#~!8_@?JH?Uu{M4>C zzMs5@t+oE*at!fp4?@a3^*b;Hac1t~45JgZ!&?=>eng*$YxAF7y;NwIsQDxgZMsIx z?CXTyGH8bi^PY_H%uY!QefIGcF8|(`kSEwP$c||;{sf*Ku4ZBIyY8+IS+MueHE>6L zwhxA!T268*Tm`@!osdGjA!EX?AZuY&$X4G2pZTQY8=^xWSNY1VG{P8L6j1>DSX61c zC+RNH0RjbUwY4l8a3N_R-bV3SXE&9d@Gz z**0(BJaxfFUE#hNfG8Fct^r12g@LZjp)4ukdfM~InZ~TW5&yQQPk7VL$3-PkyJ$6) z;PeHsur|7h_I;Lg;ya#>HC#H={C3IRiL?aNZZO*Fode zH`D+rof@nERy+L(PtUuBy*-v%OF_cg5iKF7@&UM=y*cUod^|qIR@3P8JN(zDwfzlF zVD{@tA$;AW|2d?hY3*WdVr^&b>h(V&FsrSqe)$>=iWwk9_f*AyY0^-~){C|WghMzG z>oQE?R3o)>TYHy4&0r=Z;s^#aISy?1@8k8 zY=X!e|28h~@ta*`KaVGaEi!OxNyW;dhL(Er;Yr2{{_(62VtTY(>&`G+LrdY(P~HE- zP$Y$90p5|eBFZ=iVj~e?aY6N|0myDgaj8@9AiSb)CZM!F<1o{(2 ztn=r|ZJ?>wckkXIwMzRv4yz+o6p<#NyQ?cP;QI)7{`Aer!OI5XpHow1*|lUOpcz|v z+>g|hg{hcPTE=bJ(HHYEJVWlRKoKCL$&k)Nqs9)MW@@PFi;z*&K`logvK9-V3|HNC z#2bi1`oXu&B5(F->sjmZz2RV`Z+%srOpI@{Ap;j8%l3!gkaf@itK*K_bCH<6OKLLQ zng(-sj!N+8eBKiJ+I=9889TPLyQow+w^BSCMs~8<0{L`!cs7+uWw*F$p<8=D^ITxb zukx+0Kk1coe@AekKk(#eF*3QOwr5-Y-StADh5+>wMm zU(%V&6f971-RdL;DCJ2{=jD8WR5B^m&1S>sZ&-(~)Jr;`LBGY*(B(W?;gp|x_eY;$ zrqWXSQ?y~oTz$SVga5c17SS*nO3Xu3{Yh4#v3Xl%F%X3jCd3r5Kn!Y9#7F*^@o^Bl z7PD3KYl}3q>i9ls!?^V|!Eh=el87`-p1^bqdl5122_0a0EI$_n?THRue%dU8JQIIK zLZ3iDp=uO6L2}b?^|_1A2E2w`9)#3kyhnQxY*iD=@$XjvA}J4N@%EfQKe1d~<}>&T z4~Vgn&ZKNg`P{JOLAjEj=qTZw@WxcTIx1RTR88Q2-=e4)WDbJ=&fyvzZ()?u4pS>t z60cHFk|$GJBy&kmNo`3Xtnu-00$(pa?d9v&fie6#F#p-3r*3WUC2H-eX72hAH7-g` z;R|4Z{eBDUvB1NaR~k8BXaw03J|o+;e-_<%W&8=sAQWqadG})ENQsk z`UD`>=PDp1d+x?$Qv%UpfsR&Hw%GmbqM`zgHTP%s4-iZ2gEF^B9U0vZW!Ek z(`QiOX2Fl>e&dS=%)m@|1ZDWTF;k)e?vxPZe8uE_5*U6qa`Xa$@C#N-8I!`_4jvs; zj{+TlI!Gm=!5vpvl-)6y8&iBp-av1uUvfjmIq_HOvP15Fq~qslrH3;+OW=KI$+BcK zd{`lZt+|JQlQ;f-|U zAAw{F=3Kv{6@|3QUv3X!L|Vt>{gMP9!%5F09;14+;=Tt`#D?c1}k zjR7@D7vC;WV9}b2p77%25m@ix4$kK;K!1*weu(RRzFGR=dPGWSP!KWElKg1&9c3s| z%4@MOl2G--K8;fLipo8_-W>jcH6?`aS87Nm{ z>tyY=+w-Svv&}t}pQh0l!^EMpu@ha7>{&A9>HBSOo&R>8vQ}v|hQ0)IysuY=|I7yY zKl^6>=Zbl>LO7@he6V4C1HmWqT9}WUDM43ZG%+-6;HxW2=0dGr>qvIpxC!D`Zl8?> zwp5BRG}H4<{ObEHTo8(sq#pBbwBP=TJnDWWVc5z`)}>; zfCDK-ts8T~2dU}pZW!rih{6h~5+;3i6a(sBUkdPi;KQ^$IC4@4S&TMb)-~UyY-4LOO zZw%}p>k3{xz)LW`lYihgfmacN5G&T;6i|vopZvCzeQy&0 zZKNq^qrPAbK)AN}hQk`XJ$6b&X{Wl}=cx}xDuU)Qci@2$^zch!U`l-N@PW@aa` zBwER`H)}aEmi6bT{w|eYBXMGn1AfOB6h(h(DomV86nkb0LP4Szw{k3tG3hqaiv1FU%kT;?ICHHmX8y0u0#TVuBMY9XIhK=6up5rT8 zDwatHMK6Q1rp!+1Ew&0j@BboRlP|;MoFw`iCP~Bffi&dFJ8L11ZZx`#CDCm#>0sU1 zu}rG{X3VI5 zc5hJKvae29ax1OuCxUL6s%;!M=%l|e4&=qkz2ubi$Au{&LI7M^?f^Rm)F9?S#b}&UNnQ3F?WdD$O(JO)fH*Qju{)p8g?9@rv<$A|tKu$eZ*B%fvN= z#^`}vF60HP+)2x}oYv}lleUI^j#I^WAS)yb)Ci}V7I`44AVi^*;%by$<&0m-#qpB& zRSLC4Sp;i<^y0qLj21L6hbR?9bhlkuS`81mx39G~RjK6~?Y0OVpus*iuOHv_X|TVB4ZMctQ6 z{y+0p{vV#cf0{A`tF799`Q!x>>N!~Kbyt9(f60ZvwK2GPic&+vNK^nqsa5lYw{urg zDD0f6R^5C&m!>}Dh%>Y#Wcr$59u-+6!n-3MQs`{(;zH8x-C7fh0 zih8^sY-N^vGxV$!3`ygm;QTO-GvY$Zz95)CLHgPqb%G;}m{gt73sH^?i)KKtzvnUf zD1dSU250|%X`=ZT3z2cVHcCT3ReQk+g1(%M1qEF7)VZ+dovw8QfXl8xzo{e(NZPN33uH4#XwApx<*B&)gPvcFgcm~Bt8}rZa zYV{Z}lt%G9alXWH*WF*GWzvGjZmX;~I%kq=a-c0$GeNi1;by9E!3(PS0QgDF#Vjd- z-iVMWrPcCh`mp`81+0uu_V$hKbsmn|3`?){#kHxM$VYxm$!?~&eI)8LU&(E7TOyXu z@f9t7f$j}C^d{{NQg zLQ)K~Nc$?S^NAdwiHXS_(o;@Y)KO3sL0%F zGZ?bJ0xGC$NZ8t0B3d27iA)o93?r2kbb?f_hhp2XEd$kQ)>-76@#JlzQ%S=YxJMHH zsOQ+svBFwd?C?0!d3xuJILuS|?#VbS`yIrEvshDERje@VBl=mGKx5W~A38i>nNu2R zFu-XGq!|{O3X&a!uU>;`%~`sOS^Ug0XG$hgi)B5EM&DGr3A22cx7UPH@>*zkw>h;B z!OW&=o&S{LH;181R#KY|%t?A_*27cbI8x!&TIQ~pqPl77+3hVe9XInl1&F3Ddne6U zj-k2Aafe32leDZMI$b<@5grBY)Ug1?*@~e1zF=}sk+t^Y12miVmEGuap}V$3$R%~} zlom4MW+^pE997Ht*H#!IloO*pgXC9Z8ZTjn8LJoJ?4qJ?M?~sD(7@F_4JzD$Ry2gY zPPG1@fu@)V5343j7KFnNV`eVBLWR=sxHVsle8ZutZAP7-JQ_$XX35jPvBCUjV9QPg zq?HWenofV#v2K%sOM$Bz@BdENYXf zMvw@_U;gx*{RLG|Ib0TOAouofxW8WhP0;bL8fx@EsiFUrV+>YRaQNDG=Qmq#Pz&?J z`WNWCbPh;7sT&*?Adnlm5EWUsKr)WIaKa>TbRfdcq2Q`&>su(8PykVI$~vS(D7)3Z z*DlWF?dtX0+x@Hf->#U2XE!Vci6;DS?7 zHOH-xfE{X5BH_TLxkpK)oVd8Bwt&>_nBJ_%vgGg}<_P!>WAexB-rapl5=j)Qrk)dZ zUNp1p9qF<&hf_w1GGU3Unr~49dz7>`9m7bKR02Uvu>JHLYmkn1BG z{k{BJI4WMMUg>H2M-{o1hT_ULZda-S92(RW->oQ9z5jZY#`LU2FC2A>vTH?dEkl>X zT>f{U6P6z)Om#aBsmx!9BXy{dGf|ak^HJK7A{s34SjqdADcSx%^AB)S_E;Q7EbAyIhK<66wDV zOCn3Hf@-4?Y#Bs(v&U827$wu5Gb#{DCGKPM5SP>9Y%}CJ6igZYyKCpWC4br(DK_3x zj#pvxM5b%m_0k{gi4+thZCB?_-HtA$_WNaBNg;Bz~hSZ%+~_EVVRwj84PzRy1Eiu>HQ3uL<}IT8_)wV7y%9c)Fs&ZJbs&IoMJX4OMx| z_+JzwrO7wSyN|Fp02s`u zA86W$bXIosT7ej0Ktb`Gpji)7S;EMhN93cuHZrFZ+DqC|5Tsjs9F-(q$*UYRj=&TKNcyW@?mcTB48o~Fc8QkUXH*8Xf( zxdQnzGR1KUWx6SC4-&Y$(<~8$_QI1c0^e_5#C=f+A+u)j*2A`TXU+OG6U z;ceQ&o59T|5ntmzS0Sg6wqsbh<;g|q;DMQ5kru>q!T|9FL-x=Fs@)UJP(0f_v z0&h84!!+pic4?|-eT}wsu)nmzXtzsgx6Np0h^p6!wCeUv@3#bH2m|gw9?`GwDP5S) zOL^AxUxdvpG%G(;RYzDo7hF9T$cG*syHPK@Q3?9HOYCDMyRdJa)qzz1z9j+OTR!*c z>xK{ey5X7ro9b<5{(mA*UqMg_Pg8TpeMAEY(L-b&%dsE zehTc4$chq&<>*=Ikc4c9}K&mGbz0yHurHzh6;zyAHn6IbUBvP#6R^}C%z>EU=I~g&` zQPBs8!a*);oI$c;dY?GT%Sudxuuyw*@06h1UiN!NmwYiSnt|i4!Kdx2WetYi2!w7x zV%SC)+L&8i$vDRX;Hveh-qJS1E-}@-BbS1vQ%rC+ha_ccZXUFn7|sa)LA4z9Kry4- z+}YbB)eNC+y3oxEgi}#=OUe#RgQ5{!6`d&8I8iYWn zg~P~QlvVHOz0V%dTIzFQ&#`OJTTr|u5irP3#BD*sNFUv^Fu&oqz=dKVSKTc9*+TMt z`nvuH8BL8Bg#S7+%3xG$iuh9OX1_lF?R$Z{x${3S-S{uGw&VX7tcnh!|s6e2B?Mn1WRGk6a7(l6gECO&;XVT`~|0TK*9QV4-6T$-88vXSr}FC#yz zX%$DS_>prR$)hE(;nop0J)+Ib1R^(loF<_&2`P)1qC!}D_6lc#UkegrB=j|3gnyDN z2QhaP*4M5A^b1vfph?oryMH746@yO*nIbG0gCbwk^`I`RZS#TUks!6?j|^y#z2$td zxVMjB_M5l0qHBx(oQ?`W#5=ISw zR2Xd%wEkM>EODlygve@u8rIAZiJGZ>(IT8;L(8y<@Iqh>BVlaS1kuQog+k9hJ z=MZZUXAF^G;|Bkb`4+73J=rp)%ui$+FDq+jdALyHSd_Kic^Q>#hGTZKTzmYEaG|y) zaUF#6-(@9-V)Xk=+Hc<^%>KK-_5Y3a`iBxZ$7kz_Kks(9Q6o?`eJ|J8y)=*%74Z89 zc_+~j|DE7}ZGHqXmPx3d&E>M?Fw5t{YqQLu^Y%&Y+f?4) zA)`O0`?_^x71z@R0&BP)Y?`*z=7;Y4XJ(pE14bJ^Zv&Ro*fXaWKDOUA`HO=Fj1*By zf?sgTvp|(*15#PkG$%&Hk%}uK5$lje%<+Oa)JUu*&Bl5SG-n1d>4E_BEGc}saou5R zx!P4hB_OpTAv_$yoF~jxkVBLNxHbWH3&zF>u?)QUT3}{nh!M5{cC*GZ2(jn9^rqFm zvI`Zn#$F8Adk&@y*k)Iyy39k{MPFY#YBt@(F+mMRvBPCJp%b}BTa^=50ZT?eY)j4JUSfo)BucmQ70msl%D7|kN(*8b) zxV?TVRTYSx=%g9Ben!M|iyCj7?G6A&H`=jglS{oFGv!M^RE|QXG<=%-4&IknvfKwj zeKli*0p!Y-tC|4pDU6fIdL&@0$=}?gi4R6d1cTg*2LqIoFn~|*^c1nGUmB8snGNra zj}E`JMV*4ZMg8z^$fF#NH8Tq}1vBngmbmy|C*;!jwbBT=(Ud1v@dVPZh^sX&maJ2O z<82UkU5a29!YSR6Yblxcn=ho+p&k>VIFl`@y)5kdruKo|%Ocq*&aAjVIIDEklr)c5 zSt}k;f)}SwV1>#m%|t5|b{6V+$H@0R4Z7>niscw>E@KQU3`atET;^|L9h~{_>LCV0 zUW2^Yb(wY(t~;8M+|))$HLhG4G4zUiv!D;9mwwdcGK=zkV`J2!1_ioI4{k#JZbyzV zt=TGci&;41#q?BC5nTqlsCJc&q9DDWrQ{yr;J|oK8q=0&p z<-!t=q=YMR5n)Y^L+mSVS}{`wr#`>om3(+?(5ey=#32%@Z22T6K(`DBMOIP`reYZw zVlq~v0nA_sdW{ydcE))AY>dJ|VgMb+l!jHq3=WsQc^x&o6&iL*hgvdgA660nmp~urH#q1lR<>Qx3k?Lr9t!E_ggNLZTaNYjC3RrJknL{oCs_VK@|8w3DKNcEws?q`9#i&9t+@R7 zg@XOag8^{<+`C;F$W{T=y4^qQPCWb=K3Rdq)I)#~&vkccvrhyzkVi~d2F`aZG3&%KWQ(U+c4NOG8S;CbXg zE!4R_e*fW)E~SZ~PCYaEVB_<%>hEKCnR1MW++33$?4QJBjJ!adVxd0){sCAB^+iky zX+Out2zT>}`QE>$eG+Y(r+q@fYyL(KPLQ#klgIRRIZ|GeJ+xb#-ps`+XegEZ!I7W-Aq%gzl9-h6(HiDk(0;-M>8^a%-+m|pnWx3e2C-NAn0<1Y8<l=Oh z;Vz|~USoU(X>{mQLW^dFDuoO&63V!+VU4hh<;3v z-FnxR!fNQE1+7_!{Q>q_k&b56jB5@)kgBB1r~iSurI+V-!m%A0qg^7prAi^f1G;+k zxSgE9YG0!!I_B^Gsb%*$evHE*bb|hhsB`9}<1={HaBS2GOG}uglDixO*8F&DPv-nB6qgxye#Ie?)v>p)r-;4d3IU?cqZsG@)wIZlvlixSbht+ z8EfHx1JBK0PnIFhM9Jsl#(xXeHTPoGVRsu}2zJuzIyx^VM8FF!MGuIqNt^eyJ zD`{!A4!@HTwjwLbJu)z4w|Z1>mp0;n!fmCmOWVO@*G0;DIpX@@k%4v+37e!@bgK$- znm9y!CPfuKwjos7w}-%;MbPC-&SzgNmYl~Tg&Q}|taZRp8@;cAq&Yw1t2)$@*qLO$ z%Rbh0rj}I$_aNfVv)n9XN(>9F9_b%wI8!_$?+)==4FYy1yLQBCGRQC1@Qz*Qw(?%K z%&})y$xz_nq|s5$Y{->4`ec^OjiUrJ)%Sf`OO=yim&7xb763z6%sT4Ua zdiMYq#%~v7QITEM0^Nv<2GO-Sq#T?C?aU%|luf?K5;^|cE4eB~e>vNXAUpX|h3trZ z?;l6C0a{dOZS1DiI9S#owv9*t~SPpF~Y@Jl#Bm9!fSdadB8yJ z4v8BAfJR46Nkdv1%kmp2zJkXOy^@`&^t`8F;b5f3h!k|n{?<}m-aUi)Vgh&@@MhCh zU(hsUE9IkzXT+;_cNBSkxFuGzgb4}PmORvSGgVe5fMEoPxFv`N+ucr^1@;%xnzW-n z2g2r8!kz^BrTYyA;GDIDM~bWHvQHa-Dx&gDVzRDpjWq808KE+K^t z5s}0d(a0GDzKctQrg+8o>TdO#ffyi@y1j5|tlJJp*AH3i#l9t#unP!@C08Ye)~5gY zUDQ?kY36{;k>WQ|%YUenOAFDXqOoD-Fpc@4FWf}BpZt~FMmCG+!#4U9+o#ltiBdV& zsx~ZZcn6Lsl#-G!IZQuoxqm8JR>3{czhcH+H5w7AXudukRZ>9EuyHM{7{_4x3Kh?q zS@ZDq2)Ht1oWLN!R?55%TjoU32`MA{CaryOB5)8awh#p8P3);tUcy}Rj;qxyR1fP$ z56@uTfIh2afs-086TQ%l%Av%We1BQG@F>v(>AZ4lbfAgM3NoE%6 zBFtn7=L&ve8{|7Urc05?8y``_T{d&g1?D(fHwh|in&j7QZ1@~dc&Bu zu#DQ}QyhEak z!;wLyC8`l-R|Imj2)Eothnc|_?k8khQP$;_B`p{)0Hk5d(iDKeb^RbZMH=G^aj6bF zMPU?iciY~E%wNW4((OSTS{*PByR#OEOl4rHe)ZiQXL=>1p7Q1xArMJgu6t69*~^|m zj8TwWjFC{{JUV@*w4fZmRkTA-W|oi~9;`;Y+qdO199kW&ldA4BrQXTA#M+WYR&Ru< zkErx*aa$CQ<>L#Y9zk+jae2BDbD#}RAw;Q!%c)t1r$!)rWlUK7x5yicCbp0L>7x3t za~_JItSAp~Kvrh=C$EmP41Yo?3B?vqp?PJ%y=*O$h9%AG?VtKB9WU)37uO9DOO#B; zR;CbIcDIs`%6$4R5pxjc+(nkD<6cg9Uf}v#iuXK<^55}?R=E50< zTJNY5KV;4W6nFYC;8mrhril1mkUD1q(4IApy6Lpl=E7e;pYYQ$x8U*$k7OPln-&y*dS zrgbbbq^(zK?F}$O)cw|freb$ynkJbzj`5OoUFN*tNk*;C`Ff}tIKzVsj^^jsJc5o8 zkgy!yzS)~h2vFoTRq9baVa)>`VV6oRT4KWGOE^G&b_Ud`uY!S2XgZ{j+E~DH)PJ|g zQZpP#)phpSU#e!?k>xwwb&x~m^)DzF6lH+dRIjR(E(jDFA!2(%`Ox_6`I>|uDk*>5 z7zYUre`=1amd|RX7oNSq_@@bp)%hy4{FFt2_~helXMN@ZuczzqLo=`KqVF^+j>NGl zK{u<3?s)u5%a68+Ra8D#{hKDs*1NRx@^4x~G{>{cTN`=gB-!Mgu8IcUh{HL065|MN zU2ttthAY*8n0nVf6VBVKdVpPus}-rj+9+iCR!T-CIkv!IW0|kOkx%G;ve1pzQ`x;; zXlGF`A3PxsY==5eYN{lH%ZOb2pyuVC5bGBChbahC*JJ2o3M-eVUeyD?q=qA;)JC>i zw1;2tTk)HlE>_Vq8$n<23@e+x3BE{+fWWc#tCdx3t<0NUnldH`iI<}mEt;fHZjkM&1ju}c)XgVT1aL-TGqk3t1PC7Y0yDC zC?9MOXD3IJPi^TU+4JYk=0ebfzm`)+<-y$Z=bu+CABfelO_rzCw@244`}hP5+yt%K z|BJAH4DKv^)_>tlY+DoCwr$(C?M!Ujw(W^+C*Roi#L3D3spp(Jd)MB-Q?+WnST9#~ zclF(OU!RMGE??CP#TH!k=TdCR(k$Vr>@``obWPDVwRE+~jF#awa%%~YvRdTDG8kZ1 zlS{{<#aS>dcK@=ufeY^cO>Tkv2^z4%F&n_T;d9n1xx~9Jj-;Vu8^yR$oK0>%Pa5%8 z;?6^}4L+lSWovNGR7*Iu-0;2`_8506{Y<0QfzZ@xf?Rz&!NqsByZam==%6ZkzU7D` ztlLx*fK*SI?cHcK#sun>*oGX#VJSszCMDg^J z&}2#cX0;vm)Pb%1l*q^=$1*O)DM^!WJDA$YiJ;zft?GF4feFtirJmWm*uwT`xB(Bl z@X&m2$DJe!$E}<@YhezyW>%-hf(r8lFG7!~71kR5uRKXGO@6O);&3@bAo2IW4}KQj zqUmT_(A8PT!q7B$rhmj`o11^0=>9|Gq9j|eb5tNglU%}bhI{)=p-`v(`L}RyK1m_= znX=6VQJ=Mlrf!k{LA*{hTu5O`o4}!DU#s+{ZkcEcZ_>UT)A|OOi^zq)sN|hvUcuEe zT`_fDZ4$FBIdyZ?x;Cbi6(jhlq$v(y83XP))TrQrTNq^C!9QANEN@+gIdnZqCgkgk%&|nUHrJCB-kB~tlNfnQ=kZeVK;1Le%9-4 zU4S3w%pB#@h7L|kzM_Y3-^|3U%s0HpupXnCnjwbTQag`Yea$q+rHkVBoG;&vG-w3s zM9kMtP7swI!b<%>Wk@D_5_cG?c6dp#ORQ^VuC}l$riu)oS5|0Vt5W8r-?=cyOMSowH_p>#_q^g`_|tm4l< zh-P_1$YMkmFD&PJ&V1iYJG=S55L6_{t0t=ty!&n4+61dz5*W}g&ZPpwRavdl#q!7w zK{RfI9bFfTxZ86f6v3Ss<&n`T8RF08lRs-wHTnVv1Q{ErSM~}CA-c~@90`?IF;MJr zBH~++e)(lH;Hn^h0Ab((W>|v68Bv{v28X>NYo_f;|%Zfjz50 zj0eN_`wBlO!EH-~XQNq#RvJYs_{IhICIYSQUK&~zuV;Is5c`=i4Rq1xdSeSaS{d_w zLHOwrEOwHgb;-PYEt@HrKdXO*{-aHvD;Sisn@UudFmst$902!B^J~j!kaQ@TaG#D@ zQ&BI?pR;BZGId+Sbb82WU$#|_gXE4)Nsz8@tViaU0ty(e5B`#c*hy6=C&{bXcFuX$ zx#4B^n2p(XPI?Ah^0s-*{(Q}P23+&XKj+$RJBK~<40`9D@y_1ontRW>z48QBE&@FO zyM)8byu-K7tGl1UP#!wRdPs2PJfME7`G=2p+Aj9@oAc}g?1tEAn ze)yCAh~iZfYGy0WX>;P^S!69B!!H5Xq2ZYMU?S*(!vX?xW+-5aibRbRU5yciHiZHL zqWukoJh=-NWz0x|3HdX^L#ASQIc4&`vAuU$jz6%cXZ5X#D>?8S`xl9w&X(LGe(4-| z84%vc1-4Xb$w^dEV!Wex50(lT^D3H@a7MoiEQuA-=lTr(xlpEO22d?5%Mf^Q#V=09>sWS6+)Q4)* z@$8~d)Q6IaE%<=C_h;>SRJ?EzYf4f`Ud)%8TrJzss|rdk6HRs01^AY)4p@0(8!Hy_ zV(r;QU#*#=~&w+$c?U#dIzxA(rUX`N(6raLwDmxHG?Wj)iO=EO4j8mh6hG^Y&n@Gv zgJ5~TNi5=ME%Nk^IFs^wVnBJv$(E5G;l<~;aRu!iL3V`n^`O)hxgW4T>dpI?oen0Ve%eR! zRTa!4kW-*KhvA6q zZ@CG}`jT%yBzN?}ek5@`7o2-Qo$7~8*yI8CzK~t_*-t_}9!LVAj0=pu2xyIk-Y0BBix7eZ@D1mn(p1%;y2mUz1xpUr za|Lw;{-cD?JHX=`wZ2BZrs9A`r3C0s@Xk32+MhVf(~-6v_dtk+=MGN|THy7cGI}g| zox}6M1H*>~)= z?@b+-tkGte((3u4Mr#D_4eMKi6KEQ`RBI%siAQTP2}^B-N~ckZVY}8$!bB_5JX-4G zgE5|g`J;F+Go&W60&i!G-E!ad2$5hZ%G{9@eim#z5-tHPrgz|F74sFGZddydbqA!F zA1K&f2(49Wdo;_)t~;iiAl|)6>5udQBT5`9^a+V)u3Ct6RUxL^8oJkQaDK-oDtt;+ zwH;7F84{%UTGi4xuO z2%r0%RnshR5?`{A-?N%xnnNNxkKF4-9+*ecy*|3wHNtHwiF(OP0qCpBXl5y=Xa#YG z5;u)W^O*10q%UROyJGk(L>x#txjxXK*0KN?dsuLC?+p-h9FXF9_^8qcN3vSVAur{2 zT0U0f{N$2A$6fQUZ3J~3*@s^|ySB!3n5vO^+j2d`)7oJ=8dH?DF~u(mQ+Q6|4TfV^ zmzYk*1|_*E`@|DxxAP-%=D<0_^a0fDOb3))=E#u3j(j6ebN66#SB}le5r91he{u9@ z#zV!Sp#Dz@?3tlWOv1a+&U73(TemGTrwr`T#+pgCKvXQ8Bu-yTxh)Bkvbfz zy3rYES5t)z^3e*<0Djqrle?e~$v-@&Qg;}tOI+h+Piy>_4U1lvKM9{CTdxfc!8>KZ zuJ1UzSWYWnw(Ub}@__$6PGFuWIeIZTI*t(9Cpkw*`n&k(!xs%rYmfK?bJ z`l7OrGd9d@xX#|6H}$8Ns)Bh(1}Rt8_XQn$Y)smdF6P*iCFEqW#fN%psBPXxPhdj7 zW>?gO{bNE*Pw4qU$tyl>{=f$U?;s_aSg5dN{^lXRdEhPGK`U8BkvV`S6WyHDB78!% z-%=dMdj)N}PJO)U)GFkuspv|}$#7{vN+DLNSOkI9zLJ-Iu*EcQ_m*o!l5sRV7ren( zq=I9#n1!qQA*a5cRb|Y5u&-J)x+ARfp~nTjuBf*gU#%1A>xHiv@;OQWv$JaN1k1za z$oXwEBMOC{j8dJzOB?dMpj{QY2FZ7!FfSGkiknrjA`*`}lZ^5cZ{nRh`PrS~#$Gu# z!<490*d5;}Hxl}~y1X7utu5xh9eM*0ds+{&;S)UYLBX*hdU9tsG&A~FQbPome6*oV zNGpNYSNJk&;85AV z@v=kR5H}Isx+VtSR^hRMFBxHt(!7AF3hLR{-v18=1Y3z|On8e$d0*^gM6;ys-)Utt zQHGQt=4)npPJZsZ7ga2q0M^noxBV4w~Vvnk`bXrOMJ6n``NF(kYw}li)gNI zSgmA#EZd<}(<71ACWIPP-s=cBre5fM`%yyH1(AwZ3Q1cqhL@lmtGGYx5#~d^U`&Pz z@o>j9SyYj$IymH2&xd+2MiK&zkCzruGZ2?MY4DCI^5d5WQ0;;n zt^ekhndvjzplHnR)}xzvR#i!E4NGFs;`oIobzW69(^0OF4ukKq3u0{0XFW>WzneDs zomutqEE9Si>UFp_XJ-Eb`(xL=xk|65|TFMp3SSj=d#x zgu6mtq#!I^G<2lJ!(cu|m3fwR1nGe(_93t7cv2M zN{5=p5>+2wlQ~M8%d=c+I)lpJQ#t&=sfggwknRdS#uet3UQHT8oQBn`C|l>(CAtQU zxaIscKW4K1BF0}GQDT6~$g@({d}n6(?kXwsUoN%1$WkM=4ns7Yhbow_slSJN>pu5; zy&LRVlmF@O2S+5#{%v_dJ3Sx5xx>CIy*D(kAjT)OEz0x zQ)zh7PbOnuFw@NhgyWp5zrDsQXX=2hdg7;?afQW=48(k8_z6Xi6|n9r>%LnEN)MUu zf4l5GtLcI%hU~0wK=wQaD6asnlUd!(Qdq0-G}WW}Fg4Y=Q&O$&Ix3q@t_AjiSTc&y z3*oQdutc(i9oA=+*d5bnQ0V709AV9T{!k55H%oL&xrZw^OLdD`4_`DZb_-_>24VBfW z&z6^s(Vf07vmdiP8Sx`arKGF>rS+da^a4%ORUSjFNN2~sUx~Z{8}@0WAx}rS=k1E3 zgYi7C&>TaVl9aTltkc~XidqV#-P%xo|HIjn{YSP)MTV5%LH)>K{{#fvhtPgZm)e=+ zdWpO+WAMtk2lpYO5m)mV;Z0aC+s|)KKNUvNJx)BRgOy1hiktqhstmn=#};mc0>22L zpf?q^&RO-raZ#R`WDnkNmg~DEsQ@@G-j%Tl_F??4+t8{-n+S;sHnjHB?jV$pq50Eb zx|Ah!Jcq;%tg1I2=w&tzq?3j5H;+tvj6lYe|0$l9F;hCMJclNf>fuyTM`;=L6DN-f(6l|1zD8huu^IIt?0Yg1$h`#x|(E9 zm$Ioc+r|L5vE#>jn&O_u@PCs)c!@Qqu2_ZqV=Is1kNyOk3mA>OzU15tlX91-Ih^Wl zd-<0mgzGu)5>eul+v`7ii$AQbNqTmXhPpM$mkydkQWG$HEu52A`qI0_SU9F)YA z8_Bo5S)cZFk2)ILTDxoSd2$5WD})#8^i6*k2l`fIT#Ab_iP0XtE@}NMgLeXQO#l&; z!!lbtsu$bRtHw9a#gIACa2lk5>gn6~ zoY~0_mo`$w>r)#@v(4D8^MD<32I=jqf9A zMkRIkyN0ukn3~uu@|sBLRk|e}W#nX*-dy0>txhV&gI(~Y+*2ZS=uL{NDqSZQwG?~B z=U{g-gF$*S2KZ7=*|OwNQ!P9G z0S9zmeqpVDGGF@9&?cDX=92?|pb}a^@b)k?PJo#B;Jc_NU?|*_pi>lAP39ut8RnFW z+ZJob#Mc^4xh^RYi;p|Sq1gT#y9n@GskuONa=GC7OtEFOO`JaTaL|>4IfM(ZyO!}u zX)4#)mdQD^Ij1>MsK|!VnJzMP;){uIo9E~VAe#(fQKbvD)(}c@qCD&Zz3&N98#{7< ztI8i;Zr0qE)IP#)5AU*Q>&`-UqSes|$+oPv%pa%zRO84_Y+7(~E2XnBk8xlJ$M~wQ zo-gu9ym-`0F{?`;%bIMHQsIjHJO&QV__Xnlzqw4azbVsl%6;HX?s+IZ=HtxsCOoL8 z-omF{Yf7NRop`T_&H3oNAa5^D;9l8hHXX-|xX(d#b=)N}Q+rMj{s1ss#$tK0Ql zF2auR{if*7Ij|+T{rYm?)mDKYqc4JQ@@&F6xPLqPARdY zvmH5>{#7dB6F5N6zSs9**z?8d>kBwLnlWo1su<*k9Ah!e=6j-NnUC>orkiU&@J#Z| zvdzXGudnlTWtzJ`z;4cdL;Tl0ZhI2qXzL$s7X{}32GIMT3B3Pd`!z)a%0p!t?d!&b zjM)Fj zhWur(viuQ2=3mZAFqZ5MTRWBT#k)k)zFTj!)s&gAU*dRN-XPh zmDNT?I1_~vSah9>$rKOxp!#VhnF{P9Xqd5uZUq~mLarUuDW*5Ter};Ddkn*2S#3#W z%f(i;N5IgvRQ3pXW@4FIU2VA*88e5e$4XcSj!kV;s2_5*z1UCIcAHK?C#T2XX_Cts zOB66~AkDGSO@s~=MxkaxsFv0E2e|`kaX=H@8o~uJL<=`w;<)5rWj*!NA zWcvo{cIq{P5IJV;*m^N?T@`DA8n`eHq<}a)+Nk&+%wwoR0rcf4PPL@ z!rrUTiIYiW2~zaz=tW8eYs$iy^32Ia4-6TDpeTq57$aD(MiV` z6&Knb%8YBV7#0$Q7Q5&{1uIaE6)*la7}e8hax$HfxnVhLso%iQQ@#0Mrvd~ zwpUKZ93o%;PKH`uHG&YBD2dIg?+bnPXvP>s{~}G~RzTU6R^m7mJ#NU7Xlx{-RPS)Y zCj4cwXFE>3BxDz|m=XuUj3N{R&0K6ojzlrMr%C8{gkI@#%-eO52C0@K7d6wg=BX9K8fTJj@VoDi0ftR-^NAcDx$kz_Uzuu5 z90-x8UW=GjNQIno%0Sj;w_~RoGuK@ArVjiYewiq2+I8#cX#wchFq7fB69sdhU}B*` zDMWEk@*^B{Gp0bPVVr=w?(cziGmZ;vWkO$GvOTltBbmP*(F=_5y`LmTYT?AJSUkO| zv?X+HLXQ>Ad5Qe{GGV}iy%>qDDjSoeW|iPqi@kn6pl)hQsa}tuf6y>Fb{HBiLR=F1 zd^Fi;U=)-av7=P4pPP>5?8WCR-v5g;t*Il$3SFc4oFM60kcTAc*>Ick&CGM_#@t3C(2QcByrPkf5n;)uBR;R#fZHqR7bm^Z4{ufR6THj8+w z)wjvUlBeVdDgz^XYKq{w@dEP(H8am2SJ>Wxy43}n-KQ}|vaz`&N525P;P>Xm#dvJr zgBqI<>$GPE8xiFgCs-bC5mjdPYnUKKKSe<}u04yLfpwSfhP@!=L7gRQRe`gY%$<)e zSRYP#c27myLXV`)4M)?r~xyn*sL>ERF>64PM3RF5xS4_ND@i3#23=={6yDpLc zen5zCrL~o%+|gM?(uzzJ?ZCHzgvJ~&SrlC#t1r7oo&f7Mr*ddq-5r1H$fJgDX=8g& znBUf2c0qe{>@~R=x_ysc({|v(nbkdfaRut^Q~1Rj$&G4=?EcE$rxLrlrgweuS7k@> zWkX@aObzGw<|HV6_0Taf7H2;|dGSJ;MzvDw!;As)kpQ}F=X(J^V zX;%a2=r}cMiq^U0x{As5gv|Bi92~0?&G+Z(f`V5m)k1ldb<7X}^ME@x5+C)&r`Juz z)j$)6>Tqta*sZN$W{J)fzJx~M7x@ktvBJ1(_equ)`#{O8mv0GPd2K-@=SNqh|3lmX z7u%|PzwQn7#otP~CGNR4`2@nW=7A_zfNH1<)5?Lc&a(NeQY@Wu+$^y34qB=G2oCp_ z&PD_oj$FYW1zY-7Oc*l{8(+t&hjX2~>kX7{-d{42YM$SCV-H11Ua-aNa}VrJB%#x# zwOY!na|`$!lId0z&T^$Q1(G?!t{d1~Fyj#U5q;B-58%kWQyB=vcIW?^Qcm#}ZZZg6 zii77Em_~L$n`wwg6Z$}~C?ANI$7+^(Vu)+jjDDGH_e%r6g8H{@p0>&i|sPe z=L~$`xHDbHTL|%e3VC+lz1S+ruw3yNzd<$c-1iRa*f z%bUIx^7(}K()wYP>b?imhkrWMk6SGp>47`)_gAt6)%CDRTu~bv5AxX4AyeZ%kWMbZ zjmHbxeI@zHAa8G3fKK}l2U(F{&*7{ds@Em1rM0;F^e+xzx!P1d+6UKtNgUuAy6lQ) z4({72C>{)lR~|QX!MG|E{(8fj(IZE%OY&Q#m_QSf1eZM@Uh6$-vhzl_4;F#at9Xal zX^sO&eE7pBKd<{v^K4Ea(tHP@vlWcrwUigB-<=ME=dYgZP%ivMw+w#$5DBuVb46fA z?KTPI0Hee&UO{{E2&47J6LjV~>e$)%nlE0MzWKiO^Bw2A)UWB4!vQ&_&JZL&oSS&3 zFb*fAzJr8YP=tn^1B6Rnal0{;rQzyM7=IFCCq;tL(~^}~p@Lm`ni%*s5v;H1zWOKS zXVH(8o^`Jq`^#zKEV})hL{B3{`_cyTS*svV?=jk4$6Ai@mMxwqd8E}OTwYnVT*8A~ zA+%gE-rWA_Q^Ire3@LL*YNYG^oL4)ZxOazGKkvX??}%RSgn!wiNZm5ldV7`XukYB~ zLl@0o!0T_J^*{DFxqJ`za_mXH_YctQQGA&(ZD&R9;crBmGul*)y=Bb0yR@+%!YAHN z!M(3#xl>G%=4b1}-eMS+CihFmiHqFp@2iZL9Enr*MGLZq+$lZZ=i_HflY)fa_fYiT z7?U+s1K&yZ+Azm;awv`~Qu{`cJyOn*YC3yL*?drS7YSV8y+n z#BZPk(FtuhdT`^UeQC+xyOy1>F3nr4UCKiK(ELETK6m}`FS`i0;z&2qF^Q;zBzBGN z*V&xzGa1|u4{z@XKsXv?$;1^wj~tNlot1dOjzmFix(TISyd^C%RLeZ!4E`|Ge2KzJ z_*er=(~U84`znY|oka^1WK}HJsTGXRF_FpJgxPffalf@r3Ge>grJR@Y5~Y%ZVzoZg zpi-`g;A`?mHx8{{{udhSD%yeF!0oTR)-XoYD2KZ-Vfa^ITpqIa(*EEhZynW2fWe8) zV(=<)8>7|2ZR^iyKr*H7TW&G5SvLJM&GS#$LMggd$j!kE3Sm>DmjDF|M8dU%*emWa zE7FG;Zls+wiBxl!(+=o>h_L9raNla`I3gw6fgkdO`-Dul7wY&>u39ODv%ZesCd*kC zTOb3(1LsFv!QThZ8KN%m7wd~UGk{WS@(?Oy(kNJB)V&R&;l(guSD=Bno z9&{|%YMlRWgR4cgFar4_;-vraGx+}!a&r8KLrsqIl^lv9BCoNw)M`m`WXT_&auaMzQT|f_M1^vErx&+JD?JpmgC*El1w8(Vy=&!Z)4DKK8 zb56(m(P-H$(8?SMO{h}Wxb*6dYT(J6BF&Z?{oJfee&R+CPq@McM%aKV5x_Oi>wRI> z(*&}rXsCwc-ZYUX+?de#$8NRYaYh(R%-+&m@){K zc*}1KgNjh)+a$dB#4VT{O*jht{22xB=?^;-8BL5b|2VVpz8@mESJG|wSDM&NrI%lb zSXz7qdna2Yh31&i$Os8En9g5P9CSMDn?^Vs<&i96lPA6S=)B2_mh?DY4tzK5@myF| z!(P6PgPC1{Qdyd8;aM@ujWAK?A;sp6;F8RS;L<`=Nu|6NHi~&Ei zFjiZ#1agK!NLwL6uR<6=H&Z;@x1?fdY;Kmq%8Z1F4q5@$7f^c>?~(! zYpP^wX!674`%fsc7-il6!;RuytSi|BZc(VG+7aCZ))f#$En6T4I`=+g!qj zfz2h)@rIn*e0`0TE+wljE3;0{s-b655*=2!pO{NXLPW8c`--6PCXk+83#RF``h{1? zrn?3ShWo&@p$?;AuqiebF4?uiSD_6@oC|i<`XdjWt4FID92d(DF@nw_ll!FxnB(YC z9eP%{>Y@5av3T*>h{w|WMGxY7)}8!As1$;=oN6* z>j2Wu6>jL{X<5Jml4xq6q?fdckT^$eX3|&#xG#2=U4h{_ZCL1d3S5@At@um(7P6cu z*u_HW-cEtu9GLJ4OTs`Kysf>u#{x-&QBy5RCRtl0AfekV0^@6dK_j)by!qLBfb;HD zd%D#)l6+xjNV=64SGC50QF0NrP(PhC@yj95kLg7|VA~IN&M#z+cmra5ic1J%V%Zb&Ee$d>WwQuH^&FF(GNYg5!Feliq6?c;gD01E!D5+7UXl}#HB}hH zb%HE1G{rLCzG#8)e;*+IIC5SO|D?e8PYV1mKI8v21^!c!RoRhWRKVnQ|FNH|oC^HI zfMg=U9?vA;$03q1Wg2bGnhsJ`GV&U^Ij>)gbIsncz)a2qy z?uK*cb>_2Ezpw8Th#}S-HmG%vdONjnm8PlkG}fA8GWFP#dZV_%$|5V2I)Jg*t=T=( z$kI*LQ*(>aMPDGfpvv;Xqdv;8v&5{mxMM}oa_3R{z4LB`9Zy>zo6r}@9Mg(c9WmYn zQChmn>PYrX)icLXtS2&xNZ6;pS=K*+km`huL}$I6XxE*$JhT zuC2l=FBLc6A}2x-xt(QWlbLrsyL&IQ@*t7O>Ku&h5k~$Lb zg49HMW&kkcKdpd8Gw9ei6BCRQyujZ?3#QJJtL0scpbEgIOjDkR)(o@1HQ0Q?T zv(?mQTg?Q9GM`R8G=@Y#g>rJbx7;Pq!Ba|}a1O?6%+)xkn7;BKLho5lMIqjXAA7E^ zlgwH!&$C(!JImHsVO{ix6bQ508k^d;S+x@;>TuV8+77b2o|{guJG&lqVDZFHwA!|J zr|%iYcMqS=h7_>ZOS#(y&q<4n0e@x|rhr*|VO9`#M(i@`mT-mRNGx_r>=;-C;v;v( zkZ{eTL8C$<{Am2>Pb1{;*(4#Gd)Sl1olOExACZF}pH)btF4mx!Qj7tL59El{HjkJf z6))z1?|`+a`BV2XeMbC#d6~JB_;x6lq*}xkY=Rg*JF$JX(Sb@R6UK3G*z+<^2&3*4}xnQldbHUW?i=KD3b zLP&5uX~C1gMUvL<3T#}qKy}gubuFs zVO9N>PZZ!&FQ{(u$i{x0H91588vyC$H6$7FdPlMIJro8*;M@>JMz>?WzSF+}(J;G{ zih2A4G_sC-!tk>r$&w-J#afvjquU~qNgdPN2DNUP^2J3QJ zDRTuJ=y`kA%s58v%_D@|k`)OSN=fli@;Zd^ur|40h{6UySb@9M<Q96qQh48NCWfmFhRK7{V5<3EErZujXk<=Vph741Y11X=I|5; z*5}Gaxnk^II0>KTp0RQbh4t)VUQ!qq0s{lgDg03qgO|o$8mh9UC?e_s7bfI#5ekjj zcAYNdNofcNcbvEOGPhgDE_h2aMk1L5gYMEJ0nXC?qo=jX{rlJngBgsneQ+*5+NK0M zvt7Mkx49O#U?~bky8DFOah8-8;NRh=9xIG%SE!S%2cOe|2af@+m1t^t*X(uqq<;TtKC;6+*J_!Xs4tQsPA$L8r>AvE|BD{AZfg9{SN4<*694rzoIAOv_{Sat-$7zmqdohL<%@qMz)eSN(Sc z>Pw*TThkBEhxDgj{m)F@pGMZw&RoIX#?sjHKLng_Y!~!^0HWy4OJto8jJCBc06~IM z0}O6$-FC%MQxgSH{in}t58_2}9~>qmU=bF;`OGl8`uWQs@Rwm`6ULDsg{>@^%AQ8` z(fA=3ZM9+>qzES7kt#H`GcDVCLQ32mNwj~5Xb56x=I6phBP=`Fspfr+T@Q@M2m9;k zLGhQ$xrcr7Bkt@4xKP+EhNbj?iQ4M4d+=9lJI64!afh%)eux!zje;m>@Rle&Nno{8 z^Uij%AvGG>4NLd#FpX8a$yS^p0PhD6ZG$%s7?e)*{QI7~sbv&5nU zgC1B!^iTdn<8^6b4zYNAjFdpOv^~)4ny%lxPH zuAquUvL{w&;xF+^jwXu+H8K|?cX;R5d#6-{(ywpFNbWnFVs=AT7A}hiv@9L7NN!J8 zQOA;`P#wz@uY&;I2E`^Rm$VZLi?1QKIL8A|NauSakWqWbkdylNHL8t!9^mQ@Xk)~q zW}b(qJaM>v0`x$s3dPIrw=do=A0U+<#OEK%w&1JvLW|c;I5E#ab{vD$5z)y;&K6jB!XpM1pvMzXVBy2n5(EX~&ergd)iOK+s9!vP>Kq z5zHuht;1?IDQed4@3*Q|FMn-aqt?g>s2W>)wba|xs%pAy)i*1bukEatw`^U%cRC(& z#|N)($}+rXUuT~EawnU9^L}6=EnB29O~YW%Z%F9Z)ba1$R6KEug$9_Yzg@N~c5yZ+`cIHnnw)YejSn3Xf(GoEET0^JI zSgifNgwP0)$E*sVqayM}lfpnNjEYpz4Tz9YbZ{y4RAHksDH;%&DE1ySyxEQ%0d)gI zHg6?LTE|V7F*lshKSFpbvQEF(Ny*A)$(z74DFBm6Jq_R|^1|E+FYNUr-e?>(hlT2| z?GZK_W!B|H<68qcP_dI8J&S=|nPAM%#lGJL7KFsX9F-VDF4GFx8fND0TO_o{Ea=$aB(YkBj$Sqdxme}#uVwAu%eZ!YFooSK z!v{85s+f?+c%{?f>TgwK_!>2Hm#_Y6cA%7|?+&b>P*3J+#|P*Wo}s8tOCnh%gp_A^B@@V0eX%(nyxL)0 zq5pc8IC~5w7NaSs`2y8&E}8drDarpth0SiLSTt9-3jTH}I+4gn(K?}es}B0Nc#=-l z#4N5{zS{m565Fuu@NY7UClGuFmENiVNAu<;3WE{jizkGxu)aXcgh4qksx2HGgtxMS zZkT|CP)itBm@kN#lEkk!A|^|q304KJb&!LqdrLB^k#87g(pe%<4DC6#UpXm4q$}eb zB;`||(NnB)JfvLdso(lQ!?zq~RgbQBf%Xh&yAn$%ao|-PJOg`XouYtOHRerL90ln$ ziF#i@csjBZVkq!l$+jqOADB-Gdq~9tOcm}zPbe|bPdbthle9I09EB|YKO@ih5L?tW zlgWp*9ge@}JiWb{q085SYpMbJ#rBKt(5Jso{f2CZ`eqfj5Qjd+-)d=o_j3=W99SCu z7Tzr`dW`?;ix7Dq0uC{6!<;DT$8jw9Dy)Q#SQuh^mfi2PBC&)6eqN|U$QflO9qN;8 zId}+{T?93qBPthJ@!Nc%RnMx(Ke0O1mnjw0pTO&zP=M(dcG&w{aWX$?tqtgV5r~{u zXfO5dY^J2j@JSrrBMUDC=FMC@QZV6hXJ#Wxc?4`XuRQ%&pnn2COtI>uVauJ|w+ehO zD0TDtsX5k(b2-VyN0rmp$=LdQe8)-gDoFyr?DvES+Z(AuUGsdMA_GHux+S6d+txi) zy7wu;66dr%O3B|1_GKZ)({ zBDM{bMYLkP`!~Ahf6}xKBHdj;d#6uD`Y{BmhSQZ+#DBd80+fc!FWIi#v;snONn)-$|^9F8*LRyIR+#vZ8t1S95`y{z}|a z(2|GV?%Dg2_`$p@v@QO&ynT(97>e7kT-Ze?$GB&4Y0v=;i^iN^@{O{F0nRpeXaJf@ z9`U@#ok?XTm62`~{RiqE;!_EA3fx9{P%`73Ky4Z+tuHQ~!^Tg714^ED#{-de|Iz&K z@8eK8f6?MD*qK5@zidP{uxqD@J24Y(cxhW26mKaTWQU|F@KG^n{y8o_W2qNV-Mfsr zU>0*AeZP$!h!KY zEBO(Bk#1s|8isW#vVe8gQkv=+Lk(ew7taV?IgoKQH;_j&Rgry+M%3*%#dvz_6OO0r!IZz+`Lsxrx6fr$- zvg-%)&o=S)eu6CanzbU}^I@Orj4{5(`rv)s(ETWt))IT7cV9{^v1NNqpbTl*vnOb? zq&qbxO6vrEh_`*i!UH8oufn>UE|%S?EQo%;wi&wA!j5Wpdcka*rGfJ@j*Mm97Jt1E z*qWhv(&3(s#k4zvK84t#|ItF?$xEgdikBqU)C0mB48m-xe5@GmiOk4MhPUcM zs^uI*pEhrdF1&%^5gg|R-I8$F08n0`o--$VNmPTB&ZR`LbacBI5g{kWzU6#lN>wlLN3gmnaZyI^6UAEMItaOooW-%~?H zIXlri%Mtu_@!AX?1JWkpq;>vTrPNveq*czz;NuD_fHL(V_m@?A^c$&uB|Vd1{jI?V z2cjiR5I^S0iOcWoRXK%U7TGwWluzXxlZKn)gX6Ef*1_vXot#Zg%(X+PEG;q@H5zmmuH6Q3M%!IEw!lG@(ppMOj>?$8VwR7vcywa({iUu1}r;mLkA!%Ct>E`+A+%KrSC&fmYlo=;bnPX}KFA^Z$a{>o=NO!3IXMe06Z;?e{1yH$#@;bV)^1zV zu2tqL+vY0Uwr#GmZQHhO+qP}nwz|&gj*f5d@AQs0BO~L`e;7CbA6gjLCC4%tp#o+Z@;wK8bh`3{A;HLC7r2J$^l6D=h?QD(1D}O`3Z3 zSBE|s8`Rb;f|(U4NApr&8`YMN*iRVs zrm{?aS*~#w)MRza^%o@$Tbm$%30Iu1@n4K3(-kZKS~M*-53kw5ya}>g+iP%}31C9P zkFA)iY6YS0W(mN87*I0Jc1&9=t6E%XRR2}~1j4^nBBhb^Z>c)pnCN7g*oT1*9%%bl3S&uHZK3i0Qr#cmlq**jpfg3vT)bTG z$K|l9aivlD*MV?)6AF(M^CkZW<9Y-0N5fi!6XvE*F_h(+(93CqX(n@3kRQi0Xg-jz zA~N`^8^JO>el|Plhcsh`IgcGn~Re*gd{^>Rz4VR?%Xhj0P*te%`8h>W34 zvC_%>$jN*=mpeUBWn{&4Wjep0>ET=@%j#uh1)ASk^)67~hD|=n90yLOSx%k(ywZJ; z9kB}~dbX8+$w6zS4b4d6Rrw-Q1F7jv#`mpZmH3pvRy+zU1Ib3>)^PMH!(Chg#FMy> z`wC30ZHSCIN4!P-4KeS3^<^v+UU=PFd6gqm`pBQs0yhLzX}=d2A>-ulzg*n$sK)f> z=HgeuRrB_%I<3MeG?yXCPS8H;6$e|iCNBCF|9}xQw=1|j595;xI{aQ&r_Cn14IXKB zpe*BEOuLXuRaV>`jIBz?0gD#$v~#Vky8=d`viDKUdpOVfiJtLI;rXW-!75D^LZ$p( zCpyy`i~`QFc`#8{73j}K(@>_05rskEpq9}p{QLx@d8m+sf_|jwaa3)iJ#%J$gFQ17 zNkcvS$4LWKQD$k(i8SY7@4r(kcXRL8f1;}-*UBnWD+1`wOGYq zsW^q7ly~=MkNTZN-?aja)kL^5N&26xo;vx%ro|;q_dZj>JYPy;UUPBlc$@kK_3)x` zGJ3!RWLX0uR^lf6Bnp_SpW^)O%d>Qao2w4U*u}y4*60O1zD$&XIq%~q@M9`(=7CEP z+=cx#MRP1Rdn7$ufMw0{$eTe2jvNjzf7B(MXV_OR2v$Z!oPoxkIC0pHzlIx42Dk>Z=In6d$qiMp*GbMfv)DSr# zO;7Cl-kCxqGp{wwK@HMSv1rXa8d;Tds|9ZIYCTDWe;WksbW1LVe&*F6`FKW?unIxWCK!`p|dfHf!W{VO0< z>|CE|?TUL01J}UZh@#1!J0T%0Oa~q~167~~h5tZI#cyNmsm$O)QlE^)SrYo}lo7oD z?1$I`QA9X5hb-?O2n*y)+|k%18YNxcX8s4-r6=8Q}>DFm;Xxa09hQdtzFBV-zYv@mYWl`d;a zR)(7MPoS68fjF@qWxH!=T#eFbAax=}3*>J-@fNHa!S126bDLfkpI4TgH)@kFlos#c zq*EzfFDQs}%bicI7Z+5ADnHCr_AM9+evwn%(>Dq^5hZaL1xN>d;1Ct6=CWvZ8Xygf zGS@nUNpUoTB-4n5t~0KTSBAOy_nwDe(w^Y^G{ME`l9~`1eFFr~3O8?rSHn1knU=2Q z63KW1NPoy3lc{lc88~1~;*ygJ-g^>MMInL0;RK_}oFrOsq_fvJJ%0QM#(Ba>NEs$| zBgbys*)@7bRz0L6VtdgD?*jur`}{Zp0~1@-Y6@#;vsG>t4_%1EQjxH0U=A)BL84fp zx(zRYPZv8#gx$akK>FpM>8wor=`f1z=fRt|2p}rDIFbfpP;|1U%qNLRU=HFY=4unT z#vql|`%v>zS=h+phJI{83>n`v132k{7M5)_BHqkdDfwzIft#J9??B1%2T|a=mb5I? zy876kjndS$^s#rtqwk5yb_#EwE05YKsA&}MZM&eY7Z-oM%b7<;=7segZ=`NomYQQ0jpWZHF~=F>9cJ98<9F>nSH(|b-?$7PWJg=RL>=3JVC^9OP9bl~g5Tzpx3}b^Zu1{#z ztZcWH+RQ)cdZgHlG*AB|xmoS+P_tJl_QRCJC39Q{S5ocZ8joK8?6?F6v85cBDXe*H zPmLoXUv#)Iy3*TVUgVi$R>z;1~KA95xC6+wK`WF~6U6Rhv$7H`F&2 zU#0RAlAtA74@`4$NCMO)8rq!Z%j0h;09vv3RZvF2MWv*XqV&|pi#iyw7Hk8~M#bpAA*z2ru!cm}b$vdDtHens@?p@J-#G`!DG z@{D(^$F*5#+?WD1ETORanQAOtu|R~O#;oQ>@s9d7QZaaltg=^R4|AlIP>z@E9O0b*R4=3j5xb!`2JckYfa~zrKRV$G1@s?NYCq zA|ytF=?0uXI&t>+F7>%IKT5R#-GF~_1CEA~SoNOEb6|x3eL2>zK7S39URdOPqKD@G zrTy)fbpH5wN6rmdts`Q`5oGFpxb1+G0L(tPQv@|N^ zs81*rCMx7@qJ_}tWg>tO!_%mq(CB@lfKc7TsGRWVeS%qJBbKFFS6hPWCKV=0^?+eN z>EtFALW1fkbu#JXAyrJm;dMe;g6c8VGU>!=a(-)wFnIS{F`(u*TqT$XW(f?frerh! z(1zGKz`?Ky{$-Jn13=ypxMNP8^sEr)E&d-q^rb*Iw#Yqw@&_lDgcJHK>G9UUE5EfS z_y=SJ5+pcE+e!gi0jrQjOb*vS^UF2vpa1PH{L}pR^K!Lu=J@Z2|MP+H?^RDn2U80g zB}E7Tpg~Y5i9|3c2_+X`fEBQne*<2=u#b72e@ds}KkNd-|M(-adUkpi7KRoI*8k-W zL&WduU?^a1X`^RnX!jq=Vn6bB68*eLfzwjursfgq+t-lP7DDwlr7b3WWJt&fR+>`# zB??1=B&9pvc7$YpP~JY@qNsbBiXIh$eHm;l&PVC-6XWmm3_em7aLfd$prdMVS>}rn zZl!a*xwQP|rP2EF;`Qp?VuplW9SFi0sDfyhVdn5A@R?v>nN`pVsBHX?GBYfHeQ3Yh zTTN2~#&qCNLHwQ>5OQxUL(!+iGHF}#2@ z{BxGIk8^ePb_3lp$gII0rxKm^KjAOerX5N69TfntF5#JxBAFIRJGC~t*ho9qg4ZXg zvk}4@{zAZd@o1RPN(PtaZP$fQ7*M(`wM`EZ*^Ngn9#D%L3SMX}5n&@O(05W5rqm>Y zw#o^UuEnoczgpSXF9;$esVq0>g8sV*eDFfP5V-q3`@2TKCO@~?I6yBRmUys#%Co2` zp22F7LyX8l1&!EEJxdjTft?}=%)dN~G25DAc(g~*PG>a_W2lzV^3R-Nk%`S1!ArOc zD>L(UdjFO{q+@D1R~2>^Wu?f1h6Zd(*Z6Wp&D3v>6usaLH9)<@PGZ373j37rzmBpy zU^nW|DcX0H)bGa4Ivqnb?}PU>K7$$idZC&3{vSh&>Rt1Pp1fSsYDdt-+HylHCZ|CE z$~(;8H(53P#C7&3u8jXAuEG}9djBZ0a(w@7g}dc6*3{UbDl01+rY~+jciHP78Xyrb zmBwi1cN#eFfUcXSw>)9h`cBwlb2By=9y&DO&(p)BxA3j!QcnygJ<#9r-08aQaqa#? zHG6*l$_ba^XHiy*!2zDlo!!?>Gezu3RL01ku+nERWD$rTsmR9aPF7aK6;w?bK}1JG z#|5<>LPR#?h75SB6kjF~>f4(%S~f*UYYu%OgFmu%AxNmguMKHNML245tf77+ zhz1X=d@2dUK%szZ(GWtuH*`@Y3b_z14%oW+?i!@@!=0>2r$=i5+gqI|j_arK)`5PhN7P8LT(7bp!3TiE8u5-3_+~zn)*rL9I7d@mF{834ZlKQAT&Y zNo@`iA*;DX<}@O*!({6E-yHoje^}N7qdLQWP@&=PeG!CwzhD|& zyS$K(pa6$1doADYHoAdp0;!zILRM~e&C&&UJ`l*lLKza^>_GZ0bdN9rFeZinS_G8 zFE6A74S%u`&N~{yAogdS9%sIG>n-bEH9NT(2nSAgRmrqX6ttZX%v)11PqEqoN^*TE zO889hg&R%3k4V@JBl!$*2_W-0{&lQ^OJY5h!2ke~V1LHze;liS-TnSgFVcS)ph%T) z9ff0bFK!69eey;=xdJk!VzH0{$IT5N{a^k=gQ8*q@)q?1c0T#R@ec>U=FL^>YmS#~ zR^=Mb-A>6@T_{aGXJ<>B%Ua)gmebDCA-d8FxZN4|na}Rk)9$a_Td&vY%K#6%Jb=Mc zlTnFB`mL@4*^!Bsn<0_qNoHtivDB(fMuVu1>D9{MzMbhQ_8=1Bkr-yBiuA0D>XyzEovN*bn?))v;%0cWe(4M>W zV~R=08iHQTh%U60U^xaeVvd=b(w?z<;&qYN{jOvTBt<4Of$9n7KyJu!q-@%u_ON-~-Sw9C!y_J5ns&vvZQGM}>6t z1+y+aD^g2xy6W{ZJ@(;!XV1vT?7RebJ(B8ew-NQTNhz$OPU$K@K@QlA4CzsxoTx6& zCnpNbV4n4(=>!|dk|WID==qOE6NL;haX*6M%elW zio_YBS85rscyu@p+JzVwJ$cp)b%oQ;bFka#7tc!UiU>H_mb#pU*8&0J)jT%^2i%;v zbZ$YB&|-?uWR_i{;)-&&<|d7tG1x*C63iN7HMSPUs9=t?;Gv8XQDK*XIlBA?Og+WJf;pz=B5_Ea|yQD$17%;tDKCK)}< z5-A75#ujD77qO>kD5)EwZU?ww<6DEJ_AL z1!@CNV!kvlP8kMRxj?`JOpwcty$gCpULy;+W4i*m75qFNP>Xy$Vs61CF|NGNh12%w zdd?-E)ni!tE_PI+O8tlIr6xVU`0srQy+34@lfs+a>IszkmA>?!Rn9+Lzhg1#aEBS> z57hKsxHvdE(_$v3cjMz;U7vgkYg;BS;>;45K(g&%y2;>T3??mMKxkSIrbj}Lb|@Ua z8rsAT<=)pfoZkJ6PsOgwheuG=5a?e62laawCd|IBEYa-xsl3p2Aa*2PC-aU&Kk=s7XABonHca>`tPfPdLo)+5z|AJH|{xD}eL;0M@_v-i|E-_d#+nzVo zZ(#^kd#!Jh6ul?wGt43?U$A`{8YLi%8x9$Hyl8Cpo{W|H)vh(?ztDQtUl2tWI4tm1aqec4N>dg8Xk{`rM0nf`r7uYRQPR#> zte_x{p-!mGU@pSMa9a`P)*kfi7`^cuS;TQoVY}vi?3{FE@sEt;c=-V9)+VV=;6OOl z8hjdg;?{9M=Y!vn>%AJmcoI&Utq<~#B`gOY6|pYxTfszH@$K?Iwff?A-)>eI#|As*LDphni)HQ2lw0!NNVcZCq{O%;U*A^qt9lT(ASTCn+4qI$Nzy)Z~w? z5DC@dH?1H)ltAEwC*ot$41bO1=m>X7ZHG(bodAwQ7(_ZF0}N&Z>YvgBlrjJ}wa?0U zr9D6kiN*(!zkpGt7oSU0=TRkUp>11JN^o-k3e4{LV>#kok9gYrrhx@rcH4zJ6R%HzpL=~hjX>cO+AW$ITKZ*o` z(7q&#>XjgsA)=+^s&Ez!jhO-Qv<@k`42S_+7Xsda(Vn-9X6ZF!>Q8rbjc@rD&#CDA zqWb=@490d-Tiahxj|bVP-CSTXeX!7Cnd_$IF;l2aOv%)%Wl3FyDPcvnmb0{$Rzt=u zQ&NM>698xedp;gpw61RS#;Zkf&Bcs^b!G+YXl}pt3zrh40`WanBVO|4=E3Yi6u-Y_|?2gXIQ%UYd$Ca(I@2Y_%Q_^Op zqh}%!K{XyTi|hvrA9Nq8!b}3tHcQzXoe%Bhu0&jKklJq?@`Clp?wy^6jChhMjxGUN znvOLVtu$B1UXUF>d_6uZc~%gS1*CQv4kfFTkNQ}uY9%3*Rj-La4~zC%j7^$<0rY`nRI zI&~QoONGU7ZSsu760vDIhvjzU9@ike!nON?V!HN&R}jsFo8Mc8;LF&cH#h>kjTsdj zjqWN#z|<0GV2gGQS<{X3xp)}oWx9@neMD8EQ!6VwZ;**5;ff3TaFypNDBvt3)hT9! z4*gN&ZJ2p@+$H|&;ltm(o@6^#^1K%PTm~mnjAk(xA}NS<_lkCvkj`#ejhT9Pbp9Qv zr)-9qK~MA~!Oo2p4QJ{-`Jg~mx)QbE2bH;VHhiwzfQn{~)qlEd?% zLHs6j$k3#i>2|)TjO?0usBG>CNWja(J}LM4~(TZI;$E zb_KqFgfxydm~cQA&x{LFNqa*(jwW|@JA6VAV_TF$I0X0nL_hH0(a%+3;9)mArR0_I z55bEAY|e6Q;4t+M*m@jyE#=eQD~@E z>6eL1?>7&IzcJ;Xc~muI?H4r@Dg{F}wjXGtu_)RSOrJ3>c7%p0D5iL|ZFst9X$nuF zMp~)+>*l$helmTw@xDu5kYwi-`<+Zj9k4tkZF*w$+p2dLiXd^QVggRctdN4Q4x;5N zjl8YIvs>y!It8!jJskh?Jb7lUjtP*-AQtf+9Q9n6b6%cD(edLb*P9J>BC&wRJJQp- z+Y}**-W+fj&HePU-R)ExgDsU*OEf_7axm33 zO-H{jAwVA99`!s}_AIo~U!3*qp8#Kj1FS~#*&Z^-qg@ep6yVoV+_ZqoDx!1KnEuI%DWh*@Xz-6R265XG>#|70 z-K3UM8uewLyG>_J?3qo-&~I(C$$faTO?-tk?roD*S4rmdR`TBzI-rQFIDf4LzR6a_ zbu)aNryjhy*vm`wE=twLv4dK#XlXvfHe0 zkwyf}(O!Fnf#L2Jl;&;2%r{}%a%D~Os8 ztuihIndGuVFwzs2;t9{=zJ9bq(9WK7i=H4hZA{WQo~^trR04@rr$OQPg)MZ+#jg7B zZ>46)nTb}6*@<4L^6F_vBIQadV{@=cyt%7j9tNp@wHSxsxhc@He(iCKn=GL@Mn=wPk9^FyQ-6w^J^)BUEXxu90L=#pucjT?HL6 z{VMb)uDY zN(wLxC}i^}QbBA#9p!?OAi!yDqVM`)MJGlRxU)#PtU2TxQ^gXqerY;T0RG@aOc6w0 zi2m@Z5aS{M1=!aE27av~GiG2!$jz+~AM&@XnE-g1F|6Rkuw8LrI-$QIURsc<=yQOj z%l2w1XZ39TA^zIhN8p908(^tzV9ET~Ll8!fAu_N13E059p}z62DToV_*nZIYDPL!F zNih_xDV9d87!rbGId#^8>A+Qv}5Vc1=+si$@%fH4j9U`^{csm10zbyf7f9qeNbQ5>nwzCcf*8mv| z7Jp+jLcWc<{AT-PB&H&3<5Reyy^E4;=p8HA12H-rqj_VY%oa-X3s0gz<|9(_U9)oPib z4zPp_y@8Jkb1&Qs3NmN~Iq9y(dFFil+V&m-zPati-HE2(T2($Q2D$Br&O)WNTV!QMiO5x3+Q?=QO-(Q@W}!F)=>b)XFUYN1w>2oShks;ef8jpNx5n zADHQy(h{JBUVk4_tOQSqdGGi{>8XvBR(wlS3y*lcNoH<{3Id@=dYqeRIXHJ zaIOQi+z?S_vH7GmD*^ZzC!TerZaSso9(MR)0lS5d%*{|ruleK@K)Alu3%~6_Z zA3~Q9Q*xW7P5O2?gG@UFWJEgdqPEx~8d}T-3RXlWr1Vz?c2G6*^h_=&hY-jt4dg8? z_RnO z58R`rJ@E3m5v-Vn&08c4lJbz+$YeaVZm<9C5O(c#*xdGW2s`|#Jp9i&jQ`ia;6EJ4 z5>;NE5tWd>v_;byVPwbhF2H}yGfSEDmLXB9q7dfd1@f25lFlzX;AQSi0IRboJA)u0 z+o~vT7)s@5?BB9X-@@Kb-?&^$rWJ{;&1J=>9(28TxnDacKfg_Tdw)RSMO5&1Qdfqo z;>sb4{lO8_P^X_WMNi8*;mlP#^I-&BFQkSU6i3Ga_uXOjWHKBXIchOQ4>M~dMV_ac z$-<{~=2U(OvD1VP*`O58VMt_FJmo9oExgy#9aO||mnD`>1cL|PbdT^Qf&BoSRJcO> zwhzX7(oO2gK%nF3GT+X6ssxm(H67Zd*rBuJjW4+frE`xh$Ro9znrYLR|D*2kCvS=7KTvabe5g$SpS8t!^wLybx7Qu!4B_qfXmn&gYxj&{T=1?RnE4Qj(_pmd;+<}#PjZX}mpK3FkupzO z?Nl&H1!{^`!wFw_PsQwK6gCeI-#~EiEt#PxQ-Qvm92mF@1@&kz6U9ETD1+`3n8hT3 zIZ7fSvOYQsu7C!zhs%a(Zp|ug)2}{gI2_oWK_jOrW|A6qRT2e5=mvxEXlU6|cd~=u zhB1KYEK#)^Mm$aXVnujoL~%;am~m&xcgj;}hE8Dm++!Eg6of)b#B5DYe~60@aY869 zmG5*>o$BjlpcK#V1Ql8S{5a>x>r_#@=Yc`>+l2Y^ve!0a)YnGSBT{M6v=<`3w}$?f zRlzyDmDMN{EFj8gifwJvP+O=8V>_wYDon*-QPDz0K1Q2a!dJqnZ%DZ6)$=thoV2pu zVlPocnI1Q8ZH9f#!K2>6{i8A!hU=L@j(UK0Ej97pr@ysY*x5f!6qwQjpimCqh$#Ye zYJR^*2C+wHG}@AG-j*PQ-l`!{2Wcc3|egK^~AsQRX-y4^T)Qnlm`S#Aq($zrce7^RQk*` zvrK`5PAo6Cza)@7M1!Ag%ks$CvwnNNGczjXBBW9AQ9>lZ8gp=yhBFOv&cR!#_lxl( zaMr&w^*0O|9kl@UvB8W@bk+HrL|@3v=~rQP@5Z_<23y}^>(G}plnObpDe8`wA9hDM zZL(jrHO^Dyvv<+92E90JmyleVBcMWJ)8Tvw7o}omO_D2#Igtky4aD2g@SH?Mo3>qu z!VVS2#=qR{CmE?L9oJaFh)O7!_O^0b&Z~~&E{?WMMls$3Oe(Ty^=+ySpfbE2?WIS5 z7HLF%7L7x9mh;UgH2_=9RHMW}xw)TP>yLsbZPzea&&^+t)cV!z7cUMt;OX(SPTwreC-DejbMfxOL-eO zT_UPkoOcCEEz63WJ3% zknsxl)6^y?z^&ogX80Xfo{QF>IBc^!>>-U6G0vp@5s+=)8gg(}>KCu2DT(}2Sd!AT zBR$oqb6>e`#==>1M$Ng2sI&-PwcUrHO!Ht!nL55AOi9asod|rG8=Z`0BL}O9xXiHo zFSPYxfPr54VpYkmVCO>f6KO&dL|BGA-`i@tgU&3hIx<=MqfsDjd z2}}2rC8H2*vt^1j$fPHs7xScG@gr#Y(SyX+3OAO&twVsYpYpad>UuRkI{Z9E=LNzH z5GGRKX}#&sQr7a0p}5QjCu~S!50RpnG_o0OMc2`SB8-G0kbD}9ylD(^`?77O>0c0v zl6{YVfI-KX{I+7IikJO_+RNib`#ubneMuUCJEp>mxGEPd1Ph3*tj}4(KWH+`r00}O ziWyc!H;nj!($apQG$sBO)9-8QCvYp)1*c@?@}<2`WZ3lg3Cp45jk1P)*^{UrD6NsR z(U*2sIhGSc)3NaNLjiKn7veCtPN)_62TE)FFDPw{`+kSv)~3SNiz~P>7$S;N6Wiv} zhI$cKuGE@G_lP>Ze$3WtrT2&B7()OzulRw|4q2lEWt1`5z5uGdqEipGFjbtUt-D@1j3j!R9ALO53x2APb zj{Qc>6ILHeUEnCU%Gs+I7A!L9aJoeakrtrN}HxY55ZdPp~<4B-!G{7>*g zhkGkR;&)1A3B>-^C71G-J@PSg9f-%4B8VCKf=eLYsso3L!upZ!hekj`DfvA|JEZ;f zo%w8R&1Zx7?2c76fn)E+BxIbzqf>Z(z3G^I4=`ioB4RZhpUIZ9ZU4fP-rvL5ThFxj z{Q6@(&``xvR~sM=#pGbQ(lR3ml5D9dsF9D%U+IE;9N^ynjmDu-pND{mvO)X6-*?B! zfl>wEw84}5-MSaZ?AB5na(;3@p|3q5j@ZOLqFp0voFYVk4uKqQOYrQ7*AQ$=fjeDf zP%8a%k+&!!cqLm3#V#sU^cOs6otQl?Uhc6!?940f^RCV_)_3|6YZpE2jjF!LV^3e8OgNx*YBe3JUc3;`VHocn`f8;pdR<>21Alb+BwdcW%~i zlNJLo*9g1Bm1zEbC&0nvLYUXYyj52>jiBc!olMliEEsJ`$LWYyUWF4($s7?NkB}VN zTQter;}i^?=}I~YBf^{bT;H(C2sm;i!XFDbv6cw9Lnb=9ae5C(HD;fEWyV+CzHAN- zm~(SS%MDtk@Yo20+cg5gIRf|kQZ@#rd#R?mO@JFZUI~40yTlnc=cblf8Oo)6`)zaR zJkNz=0X?Uhmiq&|MY!$%0BdD?Mg>rC|Hia{jlPrXog zw;L!Qgj_>R3v5+3jgyVTt!r$;`S&lFN8LyaQc+OH*c(OxfNY?B6b8fK9>*?V;i#I| zYLo{XXC8weaV}SLbt{m{sD7}x@JUg!&Nqns-{4)(ukt2{zV1%s3w>G}l)&n$`UiBD zh~q8M2$(tpK*D@_`9mx{uvgy)Q#4r~sx?6_1FdpYDth~d%$mKCsdu>lno>w}(!pr? znHvi~bK@VCHw3L6^)3Dp#w^$Sqa%#O<;>pSP}rmjN11~}Os;@PBDH}Aoz6@`43nnY zruWFsdBYTqes46kRZVNVn*@W25E%-^R2_Ar~cYDS5y!k%G<$QGUUA+yk zR4Wc{kQ#XEg@$dy1WB4Yx9hGKuq1y?L=tb*m@Fx*?iAlI(vk(-pCqtxsoK;H8~`jAlGdjDc40HR4HQ=Qp;QuCC>~$(OJiet750h9Tod8MZhWA>oo>wQnEA zVOatbBYKcUBUU~4hY48Y9kg?|V~smHE9!`slqA}wNLPHbrIJz!XG|ndvseeI9vQB6 zzqxiuQIXqknOG$`{%z3uiTL<5j+??U>7IslOhf510L@kp8GwAG49|M}f)+_xIn6n4 zY|+?lU{zahs8@Qw28FSeTzvyIT%z^{)8!7m%+`wY0GZ|K#8nbVsyYJJP%|>2w@Q+9 zg9O67)x%(Rfi5AIz^3 z?*CQs_*cyDKU}dZl`q|~l#stSFG%JyE#?bz6JrvR()NtfolK$pkYwW+4Gl!i5h=tJ zGb~8bVbW6_oadPDuAsdhbsqe^1E=>@c+_ybkEfiRSfp0T z4pJvt-KW{Q?XO(h@0Vq^xIlC%xs_LTR+Sumw(9Re z7vki(oky~=M%A(`@d{=%D_b4M*iFcZ%+yS_EjgUF?ljlTWJ7>?kee13F4OKWteoA3 z#=^_t15FO2`j*nb{o@M^LrVI_%KilYm5RV|cA`?*9i|8@I_o@inM=2?gp^U9kN@HF z2J*KZ=?z1eB18_O_>H9k5)^j(g&-r5F~>s(vuepXw0_NwsVZKRUzCLIjf*2E#8S(t zdR}$$57W;A@5=`w8%hnnS>RkI^Gh93k;Xq@Z;ItQ;5D>*uf8aDP-WIb20E2BppvXL zino_EsFagw%IY;~IFxMIST2paEmH>UVZY~#>eh$SvLp9x{cXyrsV$-eHg&iVATQog zlU`G9q39ZTWl!>-kC|GxutW@Ufg@kpf_d#=5@A`Q4iyWo?nxy0G7Nlrp|=pM#a0RL zQ!$yO@C~MK>SwRSe$bVkS(cSfKx$xlm~w-U^y?---JUz%_|JSh)45GJ|)8u6z%bPr{_T~E#QtAyXeRvFqO zy)mPfOG}T+smRcFRmhgI%Y2VRm!U#^fH8)o>|~y2xfa{n#A+z24<*u8vL<320}IJX zIL#ui76)UioidEvK`>^}5<1;ecWG=kkm;7#%$O+5lyxs)z=6?l$0x&^@vxb%R}jkO zX#9p)?2<32G6ElHs%IDcXh3??B-?kr(Js5%qd2s&rJREG%YxZP2Kf0+oD0+|C(+HL z5H~esx2w$OM9pGn82)2m`IPAhe-nJj@qHqDID)XR4*J?aUBZ?EnX#%aNK<@XY)0dsu;Gf(@$>N1@Sv8+E>*w z0}kRUQtu^`>XYZH>-F+i!K{VvHsq;!Sxkt4Pa&q z#vZ0CEkJ}!P8bB${Ig;p^Uzj5^-|@b&HdYJ8a7r)7O3i20!w9#4wp*IEXJOx`y!`2DatF#AH9(>SRIHA6%HX^vr2wadbKN8}aL zHlC_l=j58=69X<&s@XLJYJIZVCTdRY0haL>m6s0y9+aJ7KZODD;+Vu0XqAHbUC*n! zJ);u)@nT~A18=bbzN~{7VNM+aVQzDRD?Ng%MG|gec*1WT0-T(8ShEX^(3hp-36D; zzN%l4ZcC1?;lK8OZ6kffdxW_N-^XX-<^@JP_Qj~pstw$y)d#g(8as!(#HNS)dys(N zrQ(eki;FSdSa$>@qtBsz1*)(red0aami&s2{|080$wQX0737}edxmDpHk9ER#&N-E z-Uq=N3{P+iP2L<=#T(jo4gLbE-ph7@`R>IOJjhh1h~QSg3%?@z-PqLWzsye--Dw|w z^_X+AH6tX46FBS&_3?YS%qpg~^DAe3Q!YMYj9=_J2|KjzN}n>$+g2ZQHhuO53(=+qP}nw(ZPH+qPAmU*9-! z_dRF#?(T>+BG!);BVx`u);nK3Kse(aTk*odq9}msjjv2Dk}hIcfD+3Trz1we(#5wX z=uT^jcEUOHSb!HSAuS+G<;Yyds-g`!?q zTG}fRF85flH*om4OvWupq7-2Ipw(dP!|sn%smPq$tr48{Vv{$wt?Mri)SxNy*{yKZ zW}BFhp&3&WYaW6mEJ@zYJ}y#H5h~f zpI>Bk=Y|k_Dq&La2@Mz4#V*mf14`(HR{6z_6T*GK03veyX$1XgQ#;s%ILrVrKWNGQ z|JLo7z3mt@|3NI${69l3{tr&Ze@H-*m2VU=Rgrn%5aO(s^b9ab!b^ySqw_{eNY_i2 z!}1ZOK&0ndT(t4pP+hOC6HaxPYQ?kUOViCis+qnDIlEm6^8AK<^xa>wJ+IsOwmp6S zK5X3lGGK>Cr!PWkeXy^VTVgvP5v3bpiYdklp2FIiZz(q$3rr|rjwa+zq1!&5FQ(FK#S9anPy(AI{3&7(bQ`upw9pq zOlaTL{1frb*2u3L7)Y&>?w}sJ)`StAafDlu74%yQLg+Rs_;4NmssPK^D4YW9tHP!n z(h2f_Yl*umGK$2{@;QTO1sXBIU7rgV)lqj&a5}W%3#$tu&lAFAn7AP2X0-xX?MT*g zodnI(XN_GOCjP{|(+Z}yyD*D*w(6!1DikS0%CONigf?ZM5^1z|rDG(M@zso0YToLl zJAclGrn4l3kBm)6r^VX?%L<*}oU_H&l^_y_7EWdIWdXMIXhYOFIf><5s^R!Kdj1*> zd@~?cD4rSdhJg7dTQC(FA_;_shCyy_{%htAb|_8<5@6hF6?f50JJB9J=SKzn{l+fi zOIoC>M=srxThfJjNZOaL2_ps~R4Ospx~mLac#hr%)LbqqroWi0zoMtAOoDfp{@$F8 zY$(!bHc0Cgo#AJv&cv>Q?}L2`MHlPiyt3ELHyp_Q$^^`&^<+Wwzgs52T-XUXkiHQ8Bh`sIS`)h~GcTONNCJ&7Mexe!S*He9~6*()qVyxP4zbxH@G)F*$y z1QyygEn2$zX_Zyavx`D>aj%Q(;JPp+l9<_MXqCv9 zth7mlfooWX?olY(2=?Hc+Wp=^2O?fiULTOk=vUQ~-SVV%i?ftXFMl_)FW_^627)W# zV8JdAxGy@VU?1Lon``*@+S~Xe;N@I!-2LHBN@udNK#Y3)9p4Gv(X++WH_E= zTvvm(*Hkb6|0Op4@5#mp|2>xLJN|ea|7Wf-fTuQ_*iu7x|Id~%sK0(;{kuJs-0l7q zZ5E(SD*Dg*FXnQz>W}(@G76vWL-9CT!yS$0GyXJs-8A#K2CD?l#d@{$%b3_odrVBt zenyKq)>uM9ktYD80&-GlUI37hGo^4=8cd2z+}$mQ7Xc;Q3DOQ^1b&bG=j@lwf~|E& zZtu6u4982(4+h}Ges;z5`{k5@u%e(9*@4V(?OaONS=N_uTN`KkvTVD-l-`6$nBeRF zbjHk?Fype;=BkH#Ik7ojn4r0vWwR;{v84}~!pPFXI=aeybBsD^Y3_YwS}N7w=6tmw z#Jo&>W4YC4dmySJj-*RIPNk|gx``FC1RFAbIr<{4g+gh2(?eWQsBD8^xy{|t5R3`4 zMI)273Ic*8Q~;B!$y;)?gavXsJgG}2TR`Tl1bJz78gSZ1Vx3?!DGJ91bvZZ_1jI#g z!4%y_LM9xr1BLPe+W!>babPR?&WL-%@8WGB`HKDm$~9yFKN~rZ1eUUpGHA&3_Jm8U zxL8I*>Ilj_a4=xcnA^a5!D^R`lb0wr)(!^J{Q7Ybj8lE!0J6NprglH$ z2|6Q}%($_mMH!brkRm2b!yKXDj|O~xbGQ<6NCPHJ{givi6fGGIa~Y2aV5VVuj*}OC zX25fS0z{UQjZ8;G%`sxzh`4i8LitNS&#Hyg3*1#0i70T`bylGzEeuJIWTSY*LW?RezGbBorcs& zsbl}18B!GAH0FF|30EUHhn#+p-s*6j&-SYAypt1<#%@7~M?gr=qTIA6!yJ-%>5sN4 zgu7%8IKo|O$cFVh7unl5V{+?Y(rR_-z>674#K-N^z)Q``#tqUVOulSg{N0t#slQk{u89$=YJ6~?x z=MB?4JGj^XTWIs`N`kBq0+we(yCTlCA$5U-*OI|I66n@i32G@ifw9$kj7@l?)FLBc z{P|Ua-4=4H@$%vYCBt@T*QtO{ZypbGc(T=L;Q@PqzFUrwK*8~TgXsDjap!CF)R9(w z$%~a|DbO+#NUq^Ys-c+flBYT*+UGbgcQLxGe8Pq6OCi5vk<~36$m%XCtzPE6nPJe! zkUKGdDW+dZ!KEtEXLK63WT(&qsou!frBB@VP{cZqPe0tVn!&P*4Slh8A=mwc?%p`; zMtIAdI~2@Qp*wHVP!(1fDIKEQKIbSlwqt*zgM{lFAct{~;KPYb^5b zfFe-=g0#s-VR$~e@8fSMpmOLPS|LYsT=jD7oshqKH~bC$t(k5xU4uip0NdvKrT)1# z`?J8cnXeeT+N3_Duj-`RFg`8exj;TO;klu`jZ$4;+@##6?%abQyqfzUh`B^}?TEQ# zcLL#P5SRP2tp0cNq;|!JUx1$Wm<3^pf1zfg$&>HUs&@+2mKZ8{hjf<&jKY!Cp=Thy zQhcLQ-grdIM)0@=ntg5%G_GAMMa_cq3-8)!GH8H?O&*qvI3z3`6pXCpiNS4O4?(CA z-J>Qr0aGx$9fK}ASS3`p5bA(q`tyH$6FAG;B{6#eGiW}{I{(r)Z^^a{I*G#`76)vO z92y5~&KR}gd!}Ea8D)(J;U`MzP>ew*a)GpSxu!iIe~O-Ui=M3qAg#=Q6eG3fPq9<3 zK)w9&kQB@_Xp<^0SKg}7;Se|~ME-?>k-R!bH(&znYFo{u8~%fmRK!U3Wy)a%m(x3|z0 zWAU9yhxWt_;IYvv!3TpH3o-U5j*S>OW=8B^X^+X9a(DPw5PqaQBs-j4%)Zg1cN#>d zP2jezAz2W%w9|YDPtRW;ZJ2G)>}Og)S@yIk0x=v{7Tr6F^Cai8f-NGlJ^Hs}J65-# zfzT7uw5m8fiuo?~;iyey;Ow8c&6NUIG%EE0k=qnNY=WRO=b}1r6IY!P8v%c+e zVxs2f!MPN4?eWe4RtnuYTb$dVi(fqOWo3Ui{cO4fSgrEEIc6a%a2@6q4OZ!fsLGU?aR5(-YmQnSruksp-HTzdz z-EMSJgB#B~)Pl4V0v4!@l#iuThXV=l@aJ+pAnZEtAE z5<86aNY>GoiXHiDBaL$VD`ytz*za$U52dkcKOxvBpxURfQA8Qou%xqq$kr31#7WZS zmjou1q~X(k_eXCO1_d-`?Q-}X_5I{i0`mvs)Vg0TTodqc83ts8eBs$%h`PC&EvEc4fV0~|XnBjB@vXd5XO;sy zR3BrsS2T10R7GE;`mPBq?y+pAw)eOzM2nyHO_O+SxmD-=eTqshZ0a;%K(Yo-_j|wl zd~^^9?2!!$qve=f;aXIYTT<>_Uj^oy0oA# zGl!M5sL5U_y)ko{>Kqp8Bj1YNA?-y3gI|8NKVjhV78R6O>J_I6)FPV>Aq|V#dtDx$ z;c;M$`4Uo$xQIxB6!_rkr7*m|3^qlJE5~i5H$gJa7`6Wdq$UW)@gnWG)G!1yu}jn@ zf(F&O5UG)}-yZ)0g#GoC4!76->O+G5^-KT9h64NFrNbf)KSqTA(hBhV$%X$}|CJ9{ zqgy*^2|x9C+rC`cv`(9x29|uJjGz*`XVPh?uX`b{8zCt!SXtMT(CD>vZWu{6K#Z3V z3kmoIl<6TfFIa3aBcn|cqOKD!V8RC(JeBcrPr&)u#fZl8$;CTze+IbSbd4WdH%RkL z9B;NWy;xpvrgJ&!(t5w2GyhWRt0igU#E?~fzNw6IaUywE5qa;V!ulZGvPU6?==vZ6 zBTUwgHxe~=yZLrwE)JmfM~?4nDaGLOBvG4h;O`}AVB1y>yd)QZCWxtn%Lnpb;zU41 zWVb*X7qTb>b`U?4fkOs#3#Rvw2{ssz*ktBL=1Xy1~sSp{AlCP!wFEVY&2jWO!F z>L^1u`SurS9+c1ItX@`i^5~IWc<#z%go-}dz`0Ey%wPKhKDg-(RP_wx>_L#mVy6*B zBi4B#tbyUsYKfI{KM>fYGfU)15=;&AKuNP5=zOIARp)RTJk2NrXs!6;_J5 z|25wuWLr@x8*oJ84mU>5eHa3^rw>ta2dzy&bPopJ-JiBWL6kCoj+dCHXG3b!2NBDa zAM&hTxb;Q)lu`CV{1j2e=B5reN;_@`CnaCrsuQYBS2p#tpMmC^*b^`oVmq zk?{=vv|jcC#A%l08RZg=H;9y^0W~;6C&86f6osA&ML`7lzd!!<_3}iDg?Z&~A*^th z8=@F-_Jfg5u3UuO-FL*>?>!0c!JWnuhRC`Bxd{Up5EJ*sn+6fJar^2JS>?EiC;kwe z0CWRIT4S|g=TzE}Z@Ju25VHs9!B<_tE4p`fY*5j;i6c!>LbAG5&J+Jv=0qVh_MdG_bt>)RElx|=ml1PI5Yy(F{-QJ1W*C2PyLO^>WIKRk|Z)p);f-WAVmSy7W8c{x6hBC#j38@*nuA#0S z?O@ni=TG<`D!nB-w{|!{Drws)Q3@u7BQA3TE}{vA#oixpt0G?)AQxW>^&{jENhFS? zQs?YCU^N9rFM{j=x3cn*(;V8X65kvMteWztY>L0I(R=6-CuRgt%A0K;GYhd~^PMl> zX8WTZ^EIeG%iy`1IptVE*sLG%{F6g`5pA9upUC=-OtD7#jO#3lvJg5$oCC|LjOCDC zL@=SoknAddc*`juUByU0E)u2nab-}aV!YFTcEJZsg0{fHfVs&}1p)C89&01M2T|X> z*!9jzj~@=9EywEQLaJx$VAB0+n!rf&)WfCwmQ|K5l4Sd0=Ajh=CHNJ`o_N|O0wa=g z-o}X2WP0`P-Uxb3f0Y7%)%5{~!pEqOqy~ka z#0P_8Sn=euSOF2>;LNmk3{?=*$35jKdO@ep0k307(jP!NuYtG~eDC^K_16&l^6%}`LJwA;9}(jz|Q zchVy+v!K*u0L?h&=d~fh!Ynr{wbl)e;u_HdF@;8waf{@geN1axxosF4 zV$9~F$>OsygA_KMi-UlfEW2=y!48JZ=J6Xdo$;8XRbNJvdGJ)6JJ83-c21Iw6|~ON za;(bs6TKybs8C0chAC`x%WOl~b2gB#fyHymi?p?HQyaBkq=|Z*+L<-_;x&7}vg+@_ zx~t~R$(xZNut%Umx}3B`ut4<(1>mB#EA>-P_mt`cR?k*%=bOFbrHcC-ehY!BtfW>g zphji}m_!3k)c)G|{M#L8FWY?;vRBj%ZO=Q%_aH-#Umjx3a&&$Mx!H-SqK!a95hLI_CrSXBK%+%5#?aIV|(8}_Ia?h2O)V5}goo2&iq zvktw!e-4OayCb+Q{wFHH!PToO?VL|JFuLzkS3By0GGF!ulF>^_?BX+EAWuZB+xj)V z8qm+pxFzV1d21_8_C=>2;__u!1%>*G6`oLk&3MBXK{%~YL0HDu`^<+1a0GUlTKA@1 zx!zOnls#U@VNkQ=_~6_k_sTjvur(TW#R}ntkiGqO@j025nZp-ZR<8h_&a!AmS3l7l z*@3D;>qTk8vf^)SMNy~=nYioTG|-Sqi_OGF7#V+_HpxuU1di7jrJmGB06zqm ztDK1_e+ahDJ=VxHJc{_D$d#_V6__l&FO$2NQg+f2;qhrVF0t0EI_XoH3`})|wIW$| z)YGIOratLA-p!w#PvB*AYIsx9c+YHa=Gf!|$84qOrV&BST$l_P`AU!pSD&8Wd$@JN z`nV>>_L(Lx)$%Ywj43GwgPH%UuW`=yhh|eOMN)q?IiETZqFD=YFnchf zWAbpNiLt(>KOUnbf@d9RA)^r{96pNjZ!n+VFgiiT#+bE4V99xeis~h6qkHZT5BECK zM|eR8bHCLTsRCrC-Ib6z*&T}YOOJ~hW}?9-J4n_M8><{05lYpU^VLWg-PA>+^r!pP z84_KSkBJY*HHs4u_-vy6LMnUUoy=cZ4k!;cDcr1?%dnN|+t!cxU^g0QYsW24Qhnek z2GR7*F>0YgQ7Ytsdl1hx+^oCMRJm(B8w3JKSUrkS{ayW`jv6t_PsgG~%VxW5W zGK?&-ar5Ai5a#}p`dFfd=Stjlc0u7vTerF~MEq3pL(+d&5u6{u_H!|AQs1b^k$I^b zkD_9`c?WQZ2UHZn34(bvDk((!m5*P7lc5uQ83r_D`w%Z-qOkpoWXIlanE=c`L`QZ> z30OdV@F44#i`D)87tG{86$Ye*4-mCVqfn zZ0o4z>NQLg>hjh4fhDY^o}=8x5~*B%Kop!Bs(NdJZb0F`#%sx=U!W`O|6>MRa4mwzmKfDJQgEkEmyi6HWByl0IfJFlOGb1J|}5V;((I58OAx+NJZKQUQay;s z(Wx9-?66!p$f!2cxB*`RA$<|Jm2bkCMx}5oTRjFTegV6`y@q3Q4PL1z&S%Uyw5=FdJ86RH|VcYHMkGGJGMi+zSU=ugq5& zNMP>GOKM2SQj}895d0p_L&>xF;|KzCu@OK3*&gf^LZ{aVwijvDNGI>5ORsKzixPxI zYX^;4YQ%n2z?tyn!VV30wi^J*I;Z;fxOhp=T@a`_fLqupf=QD?K(SH`PaV&n{zyrF zI2}tgm5r?!So^?1ItB2WnP$uF&(G}>LC4?|#PNwC{!jTHsa!F%f$2}lVi>2_@~{xB;GCCT#m*TMQGmEYr` zTXSk2;E7vkNI487c#%5iXMM=tl94{nX62O%C6f&I)&cPunR<_jyus&x8a%dxPyC&k z_zs0-RcHA^L!m12gKqn~Cvfa5?rrPXSesb6Hl8*j6C-AKsJ0$fHt1-VV#8S!Gmz{n zKQ8{~5CcMsv9mTg23v#T_RIg)#n!V`RupTP*&0tVS%(2+NFp zFowz9SH&Q|YZ~jG^#NcM8gjH+it;g3#(tS*qUYLqGMspC;5C# z$P2ZRex-<`L$Cn*p`tc)`D^|3F{yW;IH!a;qkr`XM1-4sIiaJO_y%J#o| zCc28^I-`DVfNf{9)3Wh;B*OX6G*j$D{(K-AK!Z~SnVFMNx>&GX$>}%3Wt~f?CJRND z(&*bln%1cD+ud`CgZobvet1((MyW(YUz1|5b;P?VaPt(Dc>s{;1#=w!`^+!JBe zoQM6~9XogSI1a<9sGskvN*f!)x(0FU*-mt5jMM!xlqONgqf3c^KlF~BA=|Y(tXJSv zcrD5KEs6Ge?$vn&=p|Rzk_MYb8&YHithB`0Y%~R30%}g{ z0hC)H&a8um(`%lV}uEdXYZmj#H>>-y$Pt)A1=P+!D@T{lFbFdX@ZvDe3CxfL8xx+Gnoy*PMNm~8 zbXJ~xfAlNXHz(}bqm8UbE-1&QPFQUolU@lrPSZ>;Gy~^rW`$cW$eKY`^{%5CU91et z3<}Y0K3Wq6OqhL7pX|YnuYotg_~=a}idQ&hYK1)pHQE8>Qx4s0>D>xF1Ez)3Z-4YH z3oVdfgD$D+S``PySjv&u^7wao1|O}c^iK}es9^(O8`;wvg@&DEIe`?9)NPOcfo>8f zbV0jj{TTjo2IwOgK{*6}i9pI%Q+-3=iYRrvY6fs+)9dy(M}#txtmq&`H2>*vb8mx= z=`og@8?b5aUnYvcF6iwkMJpktLG08bg@{D4LA$<=>``3=ZQI5|{g!QnbJ&Ac0J#I) zVt|bp9?_fNO_=Tow^$)ukT?k7nAhN&`^>R@PJ?zCh43-_v3pHfK-0{>&X|$RJhPV( z$n7;|xdDYiVGveL<7_^1>P2^56ysB>?=~3h7VNmX*{j#qG1t{sO85FsZwv^%5ud5z zH;Fl+ddFA*JM`Ot4}d_3;xHPGe#Ql9Yn@Z89$V=EStR#b6Pk_DXW!II+p?L2=hm{Cxd^o?O?Toz^cNkc zMqp?Bl@92@49ao-ZI z#)JsqG+>4vL^dsr;FhqWwSd5yPYE^A7l2-e4{;g;u%$o)Fva>nJ@S{B8zmM8iv&0# zTc7=p-XUsRwh37|ZnQZwuq@S7aS|aqlPS8)C_^}NB3XED^c6eM#h;k7{p=ic&GV6? zgfkKc0nX?h=R^8vZ)<6ik|$>O{FKeqt<;cq;!jCwlI!2>s$BH%J>_B2-+=~^i;RFA z-X>E=ZeH!>L*^Jkc&M77B7e;&8xSRA`Pkp9Rzq2d9!87!Bwg_#o+|>jcmQPrj_#e{ zeJWU3lF`#^%ZEJ;u`Wf9u^^9UKi|I5S1pWkV*Q!0`ER*1T1>tjdJUiIfoC!yPlmv! zjCEkKdg0mnFRhzBh~1yWkbOv0cH}EX6IEkJRCbgrM@98~SS!7`A$zSU8yYxAVVH}H zh4*eUC0?5o-jeN@z_}~k|0KB95m1m zX6;LRWZ6}M1#_|`xr0<8jN&C8%l-gaJpEy)1Y4qX@04?2Ur;bI99khZXrZwI(eU0G zxWOw!SDSFN72~Ffaj9(@`if|Zili3d!ILGrAqgG{D4K&evyBlhJS8`|-8gT8B*i94 zAuS(D9GBb6Lz?)_=;7e@!WG`=6J-5r%jhz+-WlFpSkDrX73Ee(f6I4v)%&5pMqk7t z2iVrL$_mZK%FbfI0qkqP-b7x)VJ**`i%jpn?!RH|US!XCWB~OSZ2O7Z;wx^CGqP#4 zXL~Gi|1l9bi^89&@Ko#<6A3+2L?cokHpEnT0TaN~6&M+SV__S<3l=FV+Bi;RL7P=z zJ#)kxG&AvB*nS0LV2TQpT{Un`^!{jNRI)s^iJmy$77s8D#z5s;cT?_lVWXOcU^ogD z_VZ|;Oky-p-n^YPvZV~Hq4prXjScz$jd~kP#7cWX8&d=uZ2k*J`js6TYo$Fwav%m88U0AcEtoA60Kk>Lym z!b?SR7-B$OXBHvLOrkAe)h1+ptZ+FimyFqd%)xkC+8>`vxIHqmh~KN)!zLZ>;&HFj zkso)k!zM8mkR=!agV}~g?UDq|3DlU;T#eITdX7?9zaj^9hE(TUtR~sVt7NDoQ>@#` z$E%d?>x`Due$@DQhnK|98v#lhjgmfYuNfMo70v!kDCg?6Dzy1}2wM5sXG3ER3F7(tAZ{3y4n5Dy zjCl!>1wi*WMG;1%$de3Ev{%Hr~pQLUUe>X542yi{4s}Z-+USNr$D4 z=q4eD(jvxlG$ok0ZEim50mn+3?%gpki1-1Jp4i!u1p^@hfX#4&M$#B8h(I-SGqpTjF9Dv zHno64d@$&^W{jj6bZhZ1_q7Gr^@=&aA@}LUqpnGmY1EW~l36%R4%Ne~?G{)uLj8{W zUAs(qInTGaVHu+wBS7e-pmZWLHi#A!_}CV>TvlSmF|KojN&M9AeWj%+2&0k`dEJ+b z1Q(I>zyeh0ODUFBShxO{oFi?*(@hSicYTxqU#x1ju_-;XCPP%G@OWZV!DzgNn_8}- zfyR>kPmJG&IZ2GnL;4J1bm>W?wM^xrl(!M^?89l(GMDx6vj6X)K~8jh{vz;Qwab~MwQ)ro@eJQv+O)!J?9ozJm@fMI3T zlwWI3y>@1>txD$d35IFR@0R$Jg6LkCq!#YF`EP-l zj^?H|`cBRc#{XYnp7{Zt8Nokn%-0__Cd0q$I{ok66#r{q6>~@PAAlQINU_WjJGf6Y2wtBk!R1h~@%Xus z$O==>%*x9snV%vyVho6?%(`O$eM|?ef5%tl?4b60N=m~DyHGHsQl)EheUKx442@4yY`UI4=O`A#X@FuHD z=y;g3A@8NN#Q1jP+Pc}<0J;r+8@8jUx-^D~H!X7h5d^1sWefPxhj1E(=CU2hjE8Bhhc|5 z4loxSY*sTRPpwtkEt~x{M>;d!h5r9SyZr~xTQycvvGv2^_xqt%{tpeKe=x!S=0PhM z>l^(a&Bp)noRuXVH_fR44aCmF0m%~3~1Uwls9vWYHQ zeSO|R`l!^5>9IE^TUZ;CH41xjnl&=lRJxDVbT*nBCHof(td*@*K@H_>)i+dH^f{U@ z2i|Eq_s7AR=p*+|KLMH!+nq~OOv6ymWwxjo&B{gh9w7zUlXc|riE{jcf&#z`?1S>+ zDwWGuHE{>mmOc}a+ZAm8beCL+D&x(N2o{IIY?QmReH9slh}u*8LtvBTIV?JuQY0r) zmP$gB+6NGkVH|cSHrvu!(F-XYF~0nuu10fA!dllaX!tk>iAC|Qo#gxEmD;r5u1gO4l?w1GzbdYs-bMu%Hs)-zfA~0`j6U zx0)#eit5!uT}Jmk?OXQ{Jstn@QS3VbS}A@`xl&>0fHVOXk>vQ^sX*3(hp#plpvH+E zp+DZ3R;SU-Dpm@oyP!U9bEW`izUWxSiT_&w?i8*+5c+x*akO0h1e$*nS)L5%W^c8E zmwnqqc>9Kb3;yWuhl*}cU^{aV8ul?=z&Ex1g_TfU(m4FuLSa zuHI}PaZEAq3wk%BEANqTsxA`c! zz0=}akSb%(TdF)5mN)v4STTFP$Xn`!5JNGsr~-9uv>aC^G*7sQWq^Y-InqMj*#``% zNX0b{zj?DC*gqW?It0&tgsq#3p`j-}vVsW*8|`)tVDsv)jLopj-v`=)J^Y4@^Igbx z7@4!|zN_Z^4ZN;8mW~OS8fNo%%frBp){qci$k~CiHT?Ar!NOjkm~9C50f-=P+{COx zyAbGFs6t@hN>HJ1DWMT@KKjSbshbA3+>OhGsrae{tJtyYux;J!?AB%o+7${_>1m|C{`QWP%Y%R5JH*~&R|L{j9&k9H>W0ej$o~I`AOB-m@jv3nf9U|CP89M}91#d3-_~yl zprIg8+j89Y`&jw0wO#XHqd zXOmpd{KTbOl@G#T{9D*yyae4Tz(@lvWs0t0Yhk^bqMxF2K1|r^(3D+z!ovgN;c}*K zXr@gsHK=0%PIXp|-Z8)E555l#lsYLCf69-y&mc6v90UJOap*s`=RCZ=GMO2w{H@aZ z=q=OC@JNNWFdoi+bqar|ndGNa?U2|YPEmPQ=@3g0jDYaMI(TTDu!3*{zONmEOE&$d zL8r1>>71~rq-e!aiDC8PHkYJD64Hsh2 z;MD0$4ol;PeVAofC-C-M(qGlOkPxjiYZ#?_Wa|dJy>)o*f;zI8W{dIBE1iu|t%dq) z?nkI9+OBYPj--IkeA(1tvUTTLh3R5Nw?*Q~+h!elaevpw#4uAc;kIVFrj2YQPKh>$ zC|eH}Y9AP_eC*Ez?I?ONB7ie&H&Ox2*>bRuD2HJwb~!0#Vo+gD$qUpw=I=BUek~r+ z+IJ*xt`o5wgI%tnSbcawL=H(v0!-y6;K5O`vUo-4MKAmwyO4QnhRH1bsq~?fVFf`$ zY$S!kOc4Z{BA-Ohm#%IC1vJn462H)nn|wrt{N#J@(<`bKAu&(Sh$#AfJK4B+WuvSx z3H<${(Ts$udeX{CQTRqDKTc<1aXg?hYUXgvxDgj{r3u^3Cet2DhQgSIW8pzZpY1{_ z##i{U-FjJ`OGRC{ikmFGlA{L-6U=j29vzkYk?{;uShf$i>#P+wriMFYWxa|~bf>v^ z*&tXKWv!0E5}d^UE<=+xb~3Xy`kzVJe@7r@D{3n)@+0$5B?I0Y7t4Y4%Sxe)XELpq zMld3g7p)=4$w6;`gq5hb0 z_||ouZ&T`+yrAl@Gd^O50FnC6?h2}~*!0uEPtUj_&&p>*9 zBnFujFMCUpSliH`FT~N7d`jT{Le3lOwgv(B@o1;ufoF$bQ|s`Q{K=Lh#4#IU2XYFFo))3n5ztCR)IcF~W6X8QY@Gu*U}!F*Fi+*qFo0E*zNf}!dZ zG(q{+0u{GUFnpX6d|$T2ou28&a9hjv1j>&V2Jp zqvp8FhGdFz{`1y%*Ke2mR((V^XdcW#IOZ#=lN3D%j|H41jl9wZ^+phgd?}#18A8+& z%DNiIY4Xp%xR3pFd){iT!@7Uoe=0wf*njvN{x;IL`_BXVY-P)Tyi2xHr4aq)z3##w z0%Fhk1Lk>~5SydPU99+NQd$%=ZApQkNtvpQ0*-XgG&-~nLcjgK%W87CTyZIxkd0A+ zueVHZcusOU-%fU1eSzA7XhIfS&Qv@vlno25St`3$%4yTDOBO3yUt26)9`v9^TVG*o zTiUzjz2`O=Vc24&JdP^!ZPp+M;02j zZqR1OR-vmhv$mB^5GkuE`jQ^g>TE*TuOxW{zjpY#iB7=00!lVoChTW6+#8$B1}EzB z=zt4JFA|9!Sj{H0^sAIAajUQ2D!@0&T1Z_xA%aArD(hLPp3QT|Up}Uq5-DO1v-Qkw zi&h^+Z1$4KK4`2vJNo3LS;ksQqt^0G@Gh0}j76(uDPHWZ_2iJ(!Ou>UtVQ&9F^KK#4z#Cv1Z$gefR6e7Mdq=T$1z0K8v@cxJjg|Z9*iyLLCnVphvcUwS z#lv=Ey4{woN>!7;*77cB5kXAJOHkx?7h4G(GQZ(aPG5dQ&&x)BV-gMh@`j!68QxpF z$BOC-cr_qQCxR{%)zdK?8iWW*1qe}K?UTY6gv*1JYCqKHM#?AklAc^qIR&6Vs8CCP zlv5%^H#Yc%1t$gbVT4-(3g1x>CgtLsB8(2%_lrr~Hm7jbut&$-Mv20RM{7)2E@F^) zx*&8-Es>Dnj3)9iC6y7OA|s{)v5*`QNgf4mXNVlK?U%Ap3a*||!7*e+=*1d1doSIs z2y~H?S7x{n!Ki?2OPd66dh)0rC__Jis7g)XbM0ybdrRp(v6*)o_ zB8fGwZ@BW=K3RE+@#3<9az`?NT?DOc4zH|GpMVSV$=<9NuusbNo=KH;(aUg@{Drw< z4~7iK_rF+s{u2`}7{jFg(M&k{S^u^bBWz^)pEv==QQfcsa?ruDN3sYgV1p3kuJ0C!Lk#Y1)9jJeliPh7o2r4tf z7Nk7!1=Pkuk&Btr*((Len!zIZ6F3+m>cY5I>XkY+>$BmHwL9HsYC317b@0HKExaM< zg?0x=!u0TN%101+YycwdcMBlkzEr?&IgkcbtB`6+s-}n5y6AmcEMNcIRhyQaD|6%g z`jyG|pMXmLqj&J%(~)8>C?};Ad!|m?UJrrDA}DT2K!0Gd&UZh0xTc?PfI%PwVt)Qb zDRHRqs8qsuR#_==Cgql7GKQVz)SG$*q}%NFh?j&A7*Yu%{>OivGlradoG+K8A1r-j zAhX+Fy1&9u;8PuQw!0lok7u%XvORb1->7DFfc+>%c?I;D8I^e2dOI%oZD)JVXZi>K zTzGJSfwhWqb`k;M@(1CT=9`cwS@E8G#ny(@IvytUkQHRD`;kqKY7CVmP2 zhtB^Z6VuhoC=7@|Qh?RUc6DKC1RsW=^DM?1Msua~SND(sUVx!35ew&!MPoE=P**q2 z`1nQ$s7C?VxmgibzqcHuShOdlfq>eO8z5Hzr9`Z?O` z??3?@6aJW3kz>LsYf-2`ALbuWW7eo=R$X=9L34a)+?N4M(m~35)*p(9*2w*4Q!Db$tVrgl{u1U5 zF*Xy{B(JWP%EN2D1x`RYC~m29_%7e9!;%bS^GC5T9PdzwjdwEHF$q)p+9AZ2K6MBi z#ReM_ffk)~>$4~V#nB8`%OJOGTS!n2+C#l)AIWsd${XD672LaCg1~2#wsdu2^dm3) zZ7}=i0sR+Ph)Ru0o>cU<&&>Mcvr0UF1`K~ZLd3J$9N!9pUjm&`J_p(=b;RN2kO<7% zdJ7V5^~I0~q-c5*MR%u)rj$e8J3{#0ZaIkgv$2W}_+p5Pzs^4gMB#p8VV&|tD&~XS zLwtl8t|A6G1TTw?r#HB-i*JESYQ=ePS?hzp#!@|BI7GXmgtR!ajW2I#XT`z}o> zp+qiXpYhRUaWXQ`uDd8`6#gkKVLT7O3S@?#+LmbU->F5vHkrwus(R-RWZ|aouPmD# z-urylTDiF&2vVf-ZR<(Nk5imjHfw&{>GS@m#UjY3TUpy7i~zpUvdFo^|C$oVBM9g@ zIop=OD{o)j*Y_ageo@TnUoZkoX6cvG{soM91SAVIfqsyl1+z}f3D18&j~edW3TYG? zDV~An`FI5smnw#-+_n;KBbI3WxiS0%D9?_MMHcvq4!ysJ7HsLi5Fz#~AX4r|sPd9? zd+rV-;zMsQY)Iu54M^s$BP#SuQWtZ%pnCrxzl-e~@jOL6E7PeS0##teb(^p5_H$Y^ zJXc;+*I3E?N#?vEIG`oS2Wn!I!Qe*Z_Gkq>p3d$gc)mAxi`J>`v%(^2y22&MUu2G` zeU{K=36=;H%}TTdnQm}nybYzBYRW6&+z*K&vp%FB$m$AG*<9Avd?mma0M9W`dB=?0 zKel9JA+m^x+u&0u{vGM|^b8LSX%P!Wc#bb)aKsnVu{fE%{ULW-b4arhRv^kBMoLGw zR!l4%dUd!HA>sD&2Pq@bElY5q( z2W30$tHkI7V2K^-6{}VNT z0YK`x3bNSaO*E^q)<*37SABUM+}$+GAOUK3sVO3ra?@~_zo^n>rvaR!^UdABzUHifXsNCbjl$hT>C z4RLU*-?}raY9<@VEi1?6AC;%v!#djtCPq;g3c0DMSMU!Y7V6EV-YH-(8olH@>6VS%ogi)g(xH< zH@Hit#i?EDNx##B8JH0tv{FbC7|^TL_V#Ro+g2FQ&Qya#5l?pMO*5tz3Mvwa<@CeF zRV|3|^W$jdHsQeGFb;fK+=|( zrUNPIa7pQCI@&hL*GnL{9_=yU@J($g1|Z7JNx zQcLIQ8E4?Jjy*3@(TBnmeg_{2k9Z-((ddeLjdwP9*7iwb*GD;6ujMqsmjA?G+4D-y zi%>B~;2n^NvK^Odmm4wPoV09-Px(c?0U1+$fp^Zcy1KC<9+tpqF=~XhX}(sawK^G^EG`@2aKl^fF~(9u;7NFVM-zV))bj zPA!mZOHQlJwzT1*nm+4uc~@Urn`1P}p28LmnHT@2FxTIr4%BGIr6?QSIRIh2i2Wo@d(27Dcs&yrQN&y%Aw>qx$6K zDqL5-{2%V915Z`FI6CPreC0xafsF77H@JzRhbkz9B&=jf{zo%UtB3*Mm%A`n>4uR5JMZG8TumaJsO#M3v9n!&517Yvz zXM&|>T{8q$*AZ@I@yOz$7K1+M-IqJI?tc}*XpbpU$`?j?K1qJ1{~7$tdZOY&prn3! z(P7;xnDx{cU4x1}_{QevuNAwVPK2pm@tUhW6?Pere9geFHcX1_7$X%4+3Uc;Vz20sj=E$k!FnSCtr9AKG~)7^7M+ zAUeX0qZgn!Pakbae=UH3X<(+(SdkzNv4BVjfJYVrrZGClsbSAn5&~Q2+`sG;Isx*Af z#{O$Lt!c2F=Z81@Pcvv4enFa4tE39v9ZbAL+I|$D-WuWmk+$H|UjH-yEaYYedRKN? zT&_Qn{v9G{Vm)7G^DvdAX@4IV-edUBExN?{V%h0vYAku|zGwK`D5gi)p}ECw;m-ZH zV1Y_{v#X>`R7?$RFX87ZR7j)LgE*#jh&@-;g#|}@ls#w7yr5@Z)AU>Ep2NJ|$?SZk z{nZRX*v8FmuXeZ4#&OsNUN@WMKCNvmX_Tbcyy%0YoBYeY@+3Y?-Yj z!(<6)fkfa-?+?t@v(0DkuSQUUb8nS5zH5au3nhbp$AIkDwaAt3S1!p;TO8%iYcM1j z>b>4Cwgy8EqPJ2?OE<_FLO5Hlk}91YEv69>&v?a`&!k;2G;mQ9CPpkeI1k?8 zyu-jha2OCX{ryqJ7-;LxwH|QeS;ZAhyEjZ*>1W6Ud4PUi?2vT3sseVh%?SaLlhgQz zAM~KPOXoFp*%TLNX&@=FE04p(Shs20sgMVXmk2BtC?x}rFf|UoNcABrE<=4+6#hCM zQW(RXJZNau%uvX0SgPx=Re|Th40y^!262Z~WPS=-R5FHTA+61Ba>)lFpsiWW-t$=g2^XM%sakCYq!M6O{V zL${~r12>CgtBkJ?sc=9uADLC6*6jlkCAC^NX3)M?phY(ziq$N7*>^tF(t7mMW?m%$ zXc#==ErziJJkbIB0#*zfNoU2weDk({Sr(iL&fQ^Gz}SMj`bpu#y&`}1*gUUGES@2XS*#-J3+k7w(e`TNX=O&ZIB*!goxkO>!3#U)ieB$> zp5;N{fEPH7Aq0!1vM*Bv+j%ChrJAtoN73zHGAo${4fH9 zoZ{VV!1%QBs4Oio+JIS``Tm@16Q}|zC1skGLbC4Rz0{?7-RZNFv1j`6e_-H3ky3*! zO(et0M(%5dYr1h@XLo8o1jm@^nD}4^8kqSY2OvW2r7XhT$yAl%-lc+A7Az!)6y)-I z8}_+4|28cDYQ|%7-ann`vgunUuS*F#agem5GS8#J3jpTA_4m^ZP1bfWM7`5HzNlWG zuuReJ--=yh&Y!rB?Cotbk{f<)G7@rrI&vkytHE-JzF0sAfp|Oh*!?`&7-xoE0t*!D z5Q1hx$;J;0-Lx-O*pCo3tL>+XeLl31%8jwt;aIo5UDlwaE5Y z;8)Uk$PdI6rY=+k9`S! z43EXymSKuz-Eg;J)-N;*hPxsASpV6X`LaY9k%^226^VC5&OLwi9D+uKEWka#`Iww$ z5YS+@LU3D%=#i*ZR@8ApyD< z!FWDO0b)p_D%Y&#E68joO{%ep;e&N4%g5^aFyZ>J zBY^jHHOr@&_qFgSIkb>+ianT0Rx>y5=yOL(j5GKI`+=eFGd+#*o-l}4>`S#q%fwTT z*<0?8y0p1#I5ou~<0E(@*StfHS?HnQ#kYvB%}Z zjpzz{N%>12kAaMjC~jBm_*)U;K`^nFZ2_1Td$JdeHrmN0#_BnG9n8suk!Gqw0HI9u zaA0_BWCvp^oH;Ig#*lx=qfa7DchY+A9q)|dgT)Z>a2#iHWlE0LD*p^wAZBMmPO^ir z&Vdto^uUqqKwDsnVi8Z6gj1Aq%NRYcaFB`YF-!G{HVw_lhLJrK*ucg30=6krx=8B@ z@|JZ>QgFyJ>USpPJ)(maw!~HdmK}-%h-XmtB*RZIZSNsVSjB!M;t7yl&q8U+dEA}14#PIQW{ zlpp?eM*jJU7XMbDF2>OK9E0MF+{8B@hWRjDona<|8O1y4r2khNtZsGb4v!-=NVrb( zf+_Hz)d&^M?cP6Ykks;dXt`m{+s% zpOo@9RIbO~urtrCeP@Gc#@Eb37GP40)vECr@d5IK7v)#hPKWpdf#sMmF{TKYTaxG%med?8a^g7OacTN|3ZYYz-A@29umvCmbauaoN8&+ ztYDveV4RYX>OPsBvoKzyjemJ^x#}Gmr0v-D5u2!dL%)?e&lF9G))iX{*Hn>w!>|>@ zz-<+lesx;$CbkOrP@QR0O4vqKqjN9}Dqmh?j-qn2ahVrP?!JmgFHA1FgkoiB8(gBS z`smQ5K-AUx9kUH4?WJ+<0gq-KUJqRPzxz4;SYAOA6T{az0CIh-eWrnJ7Jz9_6@^zj zggwAthbK4v=0ngumTqVlf3hm&5o?_BJC3W9Jytnaja#}>bj6rkjul}ei%i22{AfiE zJ-_vs&Q05Nq7^q&SunERza``7vYR7kB$66m!L`tu1)z@-1&Gi%RP#*fHL$pma=Inj^(=N% zwYbq6n6Mbol_}7)5EA9KA?y1=MYwT%iKGhD}{Ra0soHmhuiCLF)o{c)X$Y1P!}Lwb|oTp#0gs zD|iQ0MhroYHknKQU?rGa1Xq0JaVp*2L2-~^4hTwHEEyp$&d zk=00`Xk}cJy!=C3riqw+QX7&o7)J?LlNaWvl6B}-CRy@Ooz&uV@IgVX|493k!>r~t zT2kl)l>BbTlyt0C74!OL?yfg0D_(^Q)6k8)sIa-}3w!MMzZQ4@jbMyOz(i$nXz3Nj z&*aE-J9*jQzu??{I4&}19?3s7B#mfmwrG>bQAiAoU7ann0{e;R)2`xJs^E@N*c;YG z5}jQ%o$+#YOUZU@8MbVfQ(egfS*tQaweVRRsFUS{4YIMbqA{1LL{h+pP^^p1mS@fIVZZ)FnBZTYr|&L$zHf@W)ANO!N7Gj-sCMS|j!eMQ zG|H1&Ss(4gYGi>KrsAjsd4W(g>^3Kmr5FxF_5Mz_p)?4*I!1cxcJ8OG+N1*QN6g{f zSaA3)tIXimZt{*^GgUEzu{K*j=A+LSDFb>RNcR~h8;@jlWLx+WEnl*vq3qAEmb|*o z^u(6kvS5njGOI>G|5(9gD?!%AiCK~*V%IyR{)&T1g&A!x4DF#M6fcs;Nyb|@K6E+< zcU4Q5(Qj_{rWlwK851?b|udx-nDx?CJ_8J zTmF9YUOnH#&Sr%@JF};m0~F`d#~Qk9m3akgJuX8PGaZq8AB7 zOFm@ZeSw<}Ippy4xv`=ERdx z61tSU@0KUBy)e*j_+@f{A$Ccw2}T*qdy6939TkjJzNI*ppJ_=U77?tR={?)|nc_(~h>z7Gu9lD6Lc%hNV+ zXEAplcYA-=Ba+<*{x?uuq+n~p*5kDIkA$7l4iDnYb#-blEng_Y;m@Zs)S7;uD9`LC z^3{Q3>e70@W}&#(2*hF-V%oNa)gD_ac_R9H3>R2Z+cHJVpEvyI;ObefoB4YNcsguR z^yz_CRs?mFzolgxmr2^$0haPWDx}sE4{$Ty3|!aEGm_hH?55TvRL)6YtN=r1* z$AcBmIIp1og^nF``ZY+D0>Ca!#Z{x<_uj03EEslox^>xf$E3rI$DC`Nu1J%uEfyN? zAZFsJ5e+JuHDKH)0sRlRF)vG!RZ_Aw@bHto7!Et6odN74F|5P!V4+*LRFa^?&k|6BG^e%!Y)dl}mrU(obC6O2R$}^dIhUb*Z&kbiuh* z=@2-Qs@Dx5%a^E?0*imP0o5l{YC2fqJ|p##8*TG#&(Vz2)QvHx)lE#+cn`(J4N|2~NY zD9b7Q$2Tc9S-uQ3#5h3tcU6%aj}KZ8I#QD=5GVn_!Klnkp2v{EwN=t+XJ>Xal7(Y{ z<3$}99wceMg?zEr?LfN8@|;zc*_V}{_Whgnes}FH`0exk(-4Idyz@wxUSAJj@la$; zVW+xsP^N9C7l3rC$C{0G2UMCKvA0-uT1yAX@MnUe772|buSCLt_WY^Pj_g$P+^RV} zGb<6GNLGnV00URNkxW)((yYm_d<AsM}jzv^obZiOIu6wY#D$X=;>Z zd={$4MQ<%8j;U*trRWGpqeH3YqO(km&Y#R)-ft_O+1$0dv2idSRYzV7PT_vw0b3L) z42q3T827b3)!h1pK6x?`wLq5oK-NXIxcuf(iHmHRinOWfVDOo{HT zo&2&M?p?@@@(y<^9Vyl%&|L-h=J?1>v~z?Y#n8 zBo8oQVQ+)?=rwh&#&u}u7dsd6qN?*UG$`knz~2r{-XmR%Qm4?$&Q z2pjoHdf9C21U823^WHs%N}^y1`LF@s)-u+$Cedz1)87a~SO!I?Tq___@vC9G*%olf z`2Qfx|H+dQ_G{*c|C=X7|0Ch%^3T}8*wy9#kJ$jI$f=+TBl#x4^GfNyDi$z+{ZtxF zsSG3`d-;j%QjuGULJZzk8^8`R5V}sM-1ctJ>M&dWBUuxV+vBOm1H&7LPG#bcH{Y;O zv*Xfp{dM=g@zek3Dcn`^nPrjtx>I%B%x?)RIb3%<-kdE5S%l%)_TN5ije$l-jVo(v z&TD&dSe^2oTc)inzWZ54RJ19khFTB`=79(hqW?s_1&xmCI+NV9S-=LKRO3A%sPWP? z9H3J`Z}YW;QB4K;EX{6DWSo~FD_X78l%nA1UzN*SjR_u5DJ*yvB$nwCv3UC9Wqn!H z7|HjWl;M?SQ%P!Iqx8o?B)a&wL$c5S}V7PqpBN66LDo?n0K3mBR zOWK)q7`TsdBsr5TR-2Xd8LCsT^Gm=hU1`d`Poy!uwk7O;@V5@kHd>v8D5C;k6kvO@ zCS_02v}4n#c#rvzF>O)m4&XQ?x%|T|^9;}kg`*+FFqJn^4M3MD&iiqZ=J^ayI~1k8 zMB%NOV#44ySvXI&en);aisL!rBy--*g1==j3F91>`-BIi(x?n~V97zAkzm}wc?HRJ{8GlOF^l=%bf`oR^Y4#{ zbTQbvdR5rJKOzADkzSG79kL9h2wKZqef7W)$XmsIGB7&2?as9=`%vFM*^ED&;h;pT zSR4UjNF@=Bhfic2%3)>gsYHRkiJa8RWhQkGu{5KySYSZDlTUcBd5yEc9A64Uk-&>j zj>`s_`~$w+RC-ioiEr<5M2FgLSr=dx)Uc^n?QGDN2s%PuyFeI>iRuf~Vw$$n&jyzQ z7u2eLTg?#OQz!D!mb&*%?#qv24)5MBOlF_|;#&8gsE8C6iHs8c_(AjQ#}Cr~DJuVm z(*FN!MEv*ARPn$XCJxOtxbOJx@CyJZ4nPI25s=0E^*~T=2f;`bWGp?PsG(HbNZo-% zWXxpuvl5LOmmNh*&3zamwfxqN^y#pQY|Tv&P4@4D&FjnBI@UT?-^|!|^tqm=hJQM#>P;Tr^ExW%UK{3o6l(te?RY{lC z3pr4%$6`*a^|T5h=$bT*HTN~`Ocixgj%@V2byOZWL^eZ1e6X2V89AIC!;0Du)gw$y!F_m2G?KpS z@U9;OuZNmpCcDw$YufeI<4zFJdDmSI)I&`eLnKH5wFzuvu8^D=`s&dpSm;&^Jt32i zG}R3~DU;m&OH;|I_4bq;vL7k|@P*Xl-@(Sy4BsW_{6!v|%Vnb_-IjncOx zjULD6%4L{WS{O2B)ai#+7{%kF3H^K-UR8ikZa2cFTQ;JRbqRoX`24DCUCLDtL*mk_ z1=%hWZL_Re6fQ`HSHIW=;cTj2>I3Cza!_bGez|sEGGL7jP~rgIVE=m1tK9}9a-1^B zN3WNP23XoI=OJ}2w(EzH%&q5(mYo954SPL)=e_Yd5=>ViI11lN;2%Q|N>J+Z4=N8` z;^aP8+-30Q^SZ&CD5N?)Ma%JA2AgwU`eX2pqggin+Jvy>(WMD5Ac>i&!NJ+w58Wc$ zugHy5{5$V}*NAIpw%Z-z^HXv#YrKG3q{~ur2Q-WAIIYV<+C@ou8;o0;;t`TwO}lYJucl1wbaJekuB%RHx2f{#P$#T7^kQ`$sFQOX( z@IdRZ32zV@=9&tNx$x*~YP|Z!@_4}{-Yd`I;Fug9Z04~ti!kqGZOs|W_Tj^%td4m# z#Nq+>34w65@v^e3{|4EG;lViZ_o9A+A;4G<9`nYigoUP|d;K zQWJZ9igeNM`+?!J0YB60erY541Jt4a@7OG}q*jIt724MzJS>H!g-LW-S;Q5cV_{9--J#}JQ8=bIf`qT#`sf?Q&H}}~< z-Puy=p<8e5e!}}UnV5n;!Zzfkm@9jU=jG-RcWJlp!%&UA86G;M%Wes#%|wm88lFX{ z)$iG}e(*;`h<4p*T|`b&)sd(&f!Gm;z8)zr`&qQkr(|(E-$uab+?x0_L7#`+{O07a zA)X}h1+q%a%n5sVn4*9vp4r_fsEXs?wB(shXmJw;Oy`9*(#nVQ;+L4zU80(ub|fgr zHXg(jOg6XZB(zy+rZ;t5oC4dZ^^kP>IW1(`;e^yAP0En)#37R9j})K7hZ$^%VN_Do zUv?Lj0G+wI2DQmk#)*Q`9uPg+ zx72RGxR)Uhn_+1W1`2g+f@;=47T@f&Zxs$=VVYC!>UikTgdKxK`N&i|O6q2A)xfO5 zrS#Z*#{5E2q-x6sAuCPysHWB?k71c;5Pkf_csTBp5BRA@_C7+lw!*Fp+ujmw*Fpb;I5_(RYQio#QUE#2f4cgZA)elp$$c^022OhL#duvomI1ErD>*rX zM>~v#n`UpFIW3bYkzk;!SiEAt1GA|~{I9kE*-o0jXktA^$(*4MFWX?`BF>0A{$I>D_&^=op6AZ%hRjV(wwmnGFTn|DFM~bnUt|`B>xeG zOLdmm4JzjYhJu9WOnJ-Z9Qpk3$5YHi>yV801Wk_`gcPWV?ioLCo-A`DEVUhtMVRbu zg|9161f=g`L%v~mV=d=smjvg(O1u(AE)AeCpR2A^2Xh4PWO<5Qeb@h{>dd@CJaRZ+ zjDfCe1_efnsq!1J1_g?C#su8bCGOn3(PXR0PF^~*dt~2JuL+45p-sp&(cpLT$cNU) z9+46ddwyA+N%F-oN$#Ot`DKN_Ds7=UjA(eGPj{kwrN;F)6_%1(sK8#yI|t1b0^3$A zvqrT*h&Th-LsNr-O%I!<_0CpxQw5xN2y@g+=t<<{E35Dp<;doVv23x0$wO0lx8ayN z((6KRtIj6+Z6w>+k<3f%eyjCmJlh*fQG#wj-d8x?)>!vMukOa&=5`~id>LVVhIoHO zw}Mwt!1vE*I=gbQO51L7#Pe7zS$t_~1^Gx1!GGn~LFASBkA&xaYCJgi-??g%mEi(r zWt`LrKY8hoC?)#@u;Srt$_ZNMI?UA?Ce1i0Xu(A+U1bSxr);Rq&(N-4VNdo}Gm3^X z45k?~4MOt)DG}Q@WlwmvL$q+oNqVFF$04(RNmO4EU~ECkzf2<@rw8;&AGj<6mg6nI zByzunn`j1Fi3)PRiB2yT_eH#yRv7NJU$4jim7sl;8hpqvYu*hLw=wt1v0|7oL zH8N(ly&m!wAhL%|&G$di1wm<#_H9!3H!JR`uPo&k89!W=`GM z_qn^!&Ro|92~J-?YGAEU*Y3)&*SW2w%6#d>9_KAZ6Z zyuK%N&k*JusQI^##-=*& zMy*c~W*Op>x)d>#=54PX%?2h^Hl$@6pDhV(6NwFIaVPVAt( z;`+#%=fPV)yABTC2k=!i@Okhvu0XWog6_b3?`P;)Sz81D-JgVjQ;p`bi{gHO^0&~C z(D-VAxy3V!>zHHT#z62=a9S;_58t5#3E8edVug)~n>KsUimMhGva;zwA#hEh7qXjV z#1~0`Y-G#-fa+S4Hms^V7nvAMMbKw5#w?6 zDHgnKM76T*9~qqN48N;Ovy3>=R-U+D?Zl8$)>!N9D}6|YC)d}7JuD|)I~7qwKQodP z@$J}+7r>Ajl51j-%S9FZJ`^yrD+Cjhh7kBp6T$mCHO5tnYQ+2oYEwUE!+P%-rTVOP z`JRZ6Y=s3^4IR-aH8kI=5LQ|wJX5+4TDe0NrSs8O+ApWR`J$rs7Xsl(Po1V-$)3`j z*?Xb`b2B1INU>-x-ru!$Epo@UPDF^0P$!UXJH(36^P2#%kUVuid^eXqm&-qajTd8b z(q!NY7>GcZ(s%3H0>bR|9v*0GuxD01CNr$&UhiVU?kG#v(1bUFc$%*Oq4h1undgDn zzcuourQa=%@^D3v0){+v`s4Ta{qGHvl?gASQ$qu{MKmxnVC>z$)Ry z1`9*wWvprG*N`Q$#8D1)!C30L+H-IJ%&6#Ybh^X-^;*2)d#L1-aDC*$_dQQh@u!Cb znDv45ygdpua9X&kd$13lJYvW{#HylWL~%Bea|?&LAJoucV11k)$We!uhrn5zyuM9DM*8uVRv?)7{nD@Ns&4A3wOkaIPfJZ zIpzyPA9(JK`UW@2Y6zS69!|lbPY`S!GMzlHLD;$9fXc%V@OZU3aLc(fISL}!{Ow1` zHOBd!z!exT)J1H8iop523nfZ~!#U5f+b3aCVyK1pb z6K1%(5f~7GjgLMZF#~`Ebmb($#QJ}`%~^#MAx0ri@X=jFFtb$@N%jJkU>4ONv)@L` zlLxU(JW3YcIiA-&Za(5gsSRr%rlaoRd}*N(ZZ?qb;joDXW zX}pt5KPIwktQw6CIy^Q+sFjyJGqYAg?_G}5;w8aafP%A&7Y7QGw?Z2=~}hbBM)i8bs%)c`?h0Q!~9il@iKi)fS=T@uc%jXzhHKtM4kcyCrQ zkjx+PT3EMD4tP?hP^*@tJ0H)}hjw*hiLLD1l(;&gTt(BBzlUm+>e5dNwuNfz!Tmik zmTo)i2{m~S$e)t5y);poldLbpP6+!{&kbFdVQUzqh?hxnQ`nH?3Kky3$NI4y1h6RL zAB=n!MeIz$I>(S<2rLQi(x>zQ_LhJpDDSUxh}Oiu%-LHLk~PuA_B2@mV6~ z$x>f?VKeJ@5K(S7k5Be^CuFNFu6pL&1U@sgFTJmazkwo=}Xo0b;Q#0Jh_O zv`QA}rooDN_^3j@!j^CE$dFeAi&p4$tA(q=Hy^hdB#~qS18Bxy5Bv->?K6;`V$wcdg&z3!xZ*7L;96*M8FAg9%^0N;>Iz8PdLkvneNcra= zgJ_ZjfPYkQ#W|bTzor#<^_cePIq*w4=#jKB1O~@uwdS1_N^u zqL9Uz=6NYd`wiiSj0=s09QK&-&_}_<@g^qVM`Q!lv0}#Y9)m$&U%4szu-$APf03=d zMsNO06TNns%lY(4h9?*c>S!9aL9=yhkRz_YL#I5TNr%HANuqD4ZkaD3WE5tH&xxr= z_u)W19U31Z!s$7KsXKy8e4DmIr-DPLA~tZ^R4UBNbZobrM+?vh5#|~}jCc!jw~4qu zh5qK^GBa8h=BTV2IZ$qD-dERmEA_By6SJfNm)xXk4*VnP=3y7rLabr9)U$f8atKEj z9xCR`9f>fT$Szob_W<_nSxkVftTAX$Sy!4UOF~bE9wX&9sl0s26YQy?)nhU%Uq8w# z?1l)PctjD^QyPqdw?Jr#Am4-_6ZeZIQ%1WCv!aT`i*Kb7lz8_jmr}IK4!9B>p>({? z^`#R5i&ZMb?`?FnPRs40wcKBL@A%7gJ4W;$5*MAJIJ#Wqs7a#|2S^yW5kpsl`)=p7 zn0WKbIKGRumRqLW_(wc&?@TS~=dH%8=A`GtYE-j`2g5ZISCz`Ob;&pgHkEmyk$^YW zT^b`^=JMiL8aeYyoTGB1lCx2orn1!YlfB4=O7Nxprg5`&2enJ{1@Vsc(4_eh9%UQO z{Wj&wAb}Hg>aj=GUmg!M(u(hfgyp}{)A;pxbItE2;OU+|uJa0Vu3&)=9*8{H{nhib zwn)HY^&_?>0XJShUaiP2J05QQ^3&GyK#_$p?5uul^sA7-GXb}eA0+h-!FG*sPXe=W z8k*#al1Upbi2ce7TO;eghzR=P&V=v5I@@x{``h!}`#gf{HXNg5W6O{s2@b)T-89+~ z&PIS|a>Al=b)cD6C1-fSjPQGoF!!e@GN}F`SPEOhe3RnBB!_FY8!lPMR*+sjWL$EX zBFy4{u?Ply(azyH9gRJ_JnqHwg7P*lPHXbgsY~d(@~8zHo+Gaj2EX zEJ~0~l5Ddt(Y-MB#zSSr&dm1h4Ms zC(M-g<;3DkT9k76qo`Y8-tAe zgcZ6$Wg!;&Vdz3=rlxcL1!p8KL0plT6D}nT3bR?1=Q~VvsSKllY;5rjClSmJHFJ~% zG4%sJOkGsgK z2$h0!fciPC9$Rl^B*pk%v&3+cpN&~_wSO=MwHPHK(c@C;wRf2&6HpN!_(Gjs{aB#A zaTLNXJ2gf<8C`r>KG|ODWKAJsCjg$1ze)6ZRV%)J5%mLZB80DXaLt zTe4vP^S0JhLOw0%2&V$APjDa-k(D;5)s#F)J!%)g)Q)d~+$r^BYVZTF7JFtyO`^92 zDt{vq^TeZPLmY%9SY|XOABl7NGx92@_lk15wtwo_y7xAoV?!#SGw;!fxqm@ZbuRc- z6T`S{=$?mDx*Zhsg)EuVg8|}}BAF4wxjO1Suq~yZOb9T+=FFSr%%8dNn!OvYb9{wj z%0H1_uAfRqLpnOKK4&~_*U_J0N9xyNV*-c)(tt!D?45bI+jZiyQ z`l-0YOvE!hWd2*g_}f8Nj=N&P{gC+3V))uxQ=cqxR z8d}w*N!KuTPOwZiajvt}kDemW=O)Ow5L*bELSMuq;ZwP?Fh~|D=)Au2aJ#6y0JwvG zvtnqX?#D_bgdsuQg^^G{PKbV_jS@x2d0?X8@DVrQ5?>IHO2c$MhSAMWWcG7t*IyB{ zRUGK;-sMNHk;X&bxqD&iY1SpKa2sUHh3%mulVU5J3E*cJ~f*==FUSK_Kip{Kw`%`bgr>J^%5)iV)P4xefe4O zrp-f3AdGn86{OgU3pku_?@#emqV1g)tHNHp;8<9wsN)+pX)vIl#Pv_nW}LD0&HvuY z5aHdS0e&&fue_l=vyqI)*qiZ^1byTa2J8^$no^M+{R8Hy-&&vS_7U z8G=;G$f?~U{)7d4H2;-7MZCrvY}n(P4ol!;hk`K5B59wrYq*=bH;8rBLe9DIO}_>W*8jHh8WxKn+&y%IRv{<60re+oO>XHU4WRbN%>Y z#B2K$d6i_)T2h}h+K6DT{nu1`gr#a*7IuXvhRnG<=aZqH=&Hb&nSfR4N$KrebKDgmvt+Ii){lgHWBGMFVQ?Y%!o1 z+r?-CA^2^LF!+cL#tfelh~}4|&|hYMr1173;cbnWh(d*tYLyjsrcp29zV=V6h;aN( z<7S7<`7*Jt+rfG1sJo> z+Co>PkUL4Ed-a>xhOWP(`YxaeqFRh}-m&rCa9O5*Fs}RhxiW@Mr}~6ilzaKO9@ zE8yDHbMeL*n`0f!6lF>prJ$Bse4)*o5Vo%?XT2l?D-y`t=YDh&aV6VZ2?c!sLLZ^K zi6)UPO}mNY5JjNll&@O>*i`97c9>BzLS;R(Mwfa-!A5wB#c;a3$V6V3^u8g-^-IL- zkSTW$j~$^$@$+5Dsq!@HESyBdA(@!-)`5zXDa8*(Wl$x8ojS?_xncya8lE&_RVf>C zv0Xm9h43_&#dbBh$!FArOp8m!`po`X&1YIx=M`|2*?>~Iu@-$H-@Js`_+j1sDoX0P zq&Js-^NRUa6e*!tUu}tmRvd0Az-2QYemjQRvld+sGW+)zfbP%G4Sgx488?^as+Y7H z82-kI>{PCYJLnnk3VDF~e=+tBK$djdws3dZwr$(CZQHhOblJ9T@3L*Hi(Rg+sxH0y z&V7HpbIyNoBQhg)uDv58_KcNljhu7LFQ>M_$XWp1jF4%>hLWYdU+c~` zc#=-{vCfs2xoJBF8noo4BbMl<%gXf7o1Jc4svU8BaF`QVLd z(y<{C=$gc1UAp0@WjSnLn3I8rp!x7Hv+9sHA8ALhBt#Ue5!g6vZ03}m$EEAn13@(r z6#Guq5|X4ef*{r-Hekqh-ED#lim~_30$u}hn>{CSQ6&~k*Kk5R4|MR2o3}H)l zcAtLXcuw;zVO8Yr;D;Upf;GN>4Z1uj9vq@PX>R_b$^p;7shp91irsmzg7Gfwr@{ z;#;>4VF<=8a$CDWQKQsSYgg4mlxL^2G`f!VzSAgn88xnC$an?Spw`_@5wQ zQ7l6?65;W~Kkh)G+(6vb({uf_0}=K`OXDbEYJTbw(iYVtsb^A%l)NxE_!Ukh$@p_y zSW#+xG3C71teG16GYU~0$Wd)SgORp}Kgj0Gq#kZx=J+l5tugPGk^ECb z`5B|HAFtN&A2^B72;UUKWvkb-6Mx`C&V%N$pWb`twBc5hGkpXAF)UbXN9As<)CsRP>2n)|d0;q-1UA}#vSC>lp#5gU455IqUuEUl}CG5&J zD}Ae)sCWPu#5%939$kzzlF1Z9MzW&MbSO{o?5K^d`n=o06JqT9lF7(&h zjx?a)1HW~PZ0ONRTNo;X1;qkOrMd)R5R8x!5o14g$ENjXZe4f6uLtbO%Wmy0q;a-} z6hWjm2+WLwi5@&x+OM)pOu0gPEN(%)^q5rWq_tfwP3(;gWs1eN?@`EAPR3QnVSDb` zdM$FzYisw~zUNf+G!_&E)OJ@$Y(?$pEiT*i38tgO{_esOFHXeXU`9phVaPC_e#2TR z0^f));ak9uR6w1r^%^nE18RKNsWJX49 zceocR=j1NFBxTwy4bMquc>ch$o;HK(#AAm&3wQ z;jH;iHsbpyf08R-$EXQMn$RiLZWN;qods@xfG+7N2#YE4#TgZ#z{`9)PS^uA369~8 zicb=OxT^p&Na#vAI!Uc=C98T=lnsmQxMwdM!8yK%c$MiE$-{iSO%Iy-XWb&5DMoSB zuLZTw)3<&SNE5Zt8VPyItIEV#(7GhkqoP5PUCbC; zI@2Nr6itXkz|b`_W98=nxW4ZapXKHNIR3eeME3a_d(N4*{vYt?a2DR(tDi>ssTuLk zeR)m(ANQ};-+zgdJ8m!n-XHf4d(iL-}V=*b42m%>H%woC4str(_8*Z1XGg?VJ zFhhJ8wT&!rH)(K<48VAE^rG}K^}=$;_c}&0G`%>zm|=by!#d8Y#&9ORjk8V`%M7i!T{n_%9=Z^COesW#zgWAd_w@3>(PtV zqe$m0Vlc!^<-F6~pwWI&QWRnjP72X#^esPDVx1K|k5N>OCY6@WFjK5aUsl?W%TPR# zos8#&k#Z3(BdeC`O~-b++dvPAdS?ofL&Imxj$~7F8*es~${3g0VWJj}J=pi0lu>w+ zP9gN|Bp;Z2`Hl+7%LK%e9i|7ySb4#N3i&Q5Wf0({-fMd%dLECp?9GQEGeU?1mzWoi z`DVh(s9dSR#OIhUZaTIGsQRNK7OlK{`^08t)N7UDjAUW)y-11i=VHknPcWM87IhEg z)A*U2i z>+2+~hl$d|r{y!FNH6j=itb=VK457UhM9D2A-4pz`NAV=WjXp_5iw(H-$Rr19q)Mb z$wH0wo#VNlIHnt-ak^%b@DJBuGc#13QWFfpSxXE>Defry>v8Cpu5irSiMgH>_>9qNx>@}P%4f1^O{Zkl58-n3FKJPp$4)#p zAK|&^9ZyU#B{6WS=DORhFAM&F{&^eE-|q+iD7p@S1*U`9m>XnY4Qrlc^RJ@b{VBA8 zf!%7<1#5}Mwq?0UixSw&_h65RN;U=&wZY`Z+qPT|;nH#NezS4lW{cQ0+!bGcOuAID z(lg0%1Kt3~2fU?HR68u&hyW>9tp!5?5N@{2l_>qHaZg4(dZs#ptnE;c^W%KH= zZjIuKGsP{euu}1Tnd=z(5vh#e7__xQ#1u)(|})&-?; zp8tB<$VPvbxo}sBw^T0XrzN?-xWw5VI>qwuRXfCFzvPC(B6)6Or*l}|J`lp~YN_Rd z^Exafy1S9T3G!AN@+QKLcxBs5Ja~nypc_t$d`0#5_#QtmbTgWw@6fED5Hgj(pmsF^ zEJYAg#$UAjlQM8_|0uYM)kd4t9f5KPl;~Pg<~ICom%m*q!3x631rgm(TX{wI7=v|G+5ulbpgy1retw|p z8m@wW3;(oNzL{i+z$4Fdl{mI@Bz!y>8~-NvvT^hQBnRh9Icww7ga&H%%2UJkcpL0K zy7mRr9Y3yi#4jX*mjLh!Q8YwGf)-+YnbxtfQ_hmMdOD+5thP(^2_^4$??CLMQ^?8n z%Mr!lTeQ|S5*J`c8wuP8+9Nbo!r^!=tLzmEQ5&3c-ffyVdjeh0);}1NID%dDS6B{)F9K}!F9sPp{YFWIrZ9D^{7AuDOC<0Sjl>(j#* z*ddHK%otIM*`FzxV!!w7nEt2g!Ewi=Gu@&LuyOk6&IU8GyvR>L{GfoY-y4Op$hx-D6anO|V{*m5(_>oC%{uOt+FNJQxe?>tR@^mp(Gd2E4 z>xO?OpysGb+b;?re6Th2$T8FiGulQ7Yv)!7Lqe1zl)ys?K~?Svl!vsF&=S&0B~h_T z)Og@J?Ewgp(BW_bq&k9zq{}Ng90T|V=)arUj>=UfOL1sj&s?_c`Y<<1`n`R0e%}L; zV?nmh4 z6^+)!(gZsO2$>GxnM0I?nX!l<#0mlEV40F3gx01BN*aZkVSLVdX^j|Qei;A3zZGY7 zC_i|>8(73GS=I|1P;m=)hM@xOVAQW!FOErFkNO3cTdO8TbhDfT0rG5iHNhFCTyd5U z!hX3SRKn7^Xz6aPQ$Q3T;)0*;@R(nH2ACr7QWESm!puKPi&H-jG$WfUyo zXc{0(h<_Hy5zFB3r9Xb>H7-pq>##P4tt9mgBZO(fGl~?OVd5_w^#XrNXqY8VFFs_N zHU}B-P=bRniQnr+NlSdJCvZe3Yaj(?3)9n|AyE~uYKYCiU8{xV4sv-kBxc`I#C~d# zMNXSzJF`*-Vl&mYg}Y_rdiBX+&Yh{MmBVAWr~T`y6OX1%dA7XkwFje)BB&h{R=uaT zHAsX;_wn)R0ysW5cC6M&*zMpYY{#zLR*ZP{lRum-jrjXw%7mU$1~Ci2ygy)ou|Rz$ zi?%~in(e-JC-?Sup?!s`hX|X=J#1Gt+mSp6S0P5!16EI1A-^Z|HgW-rldKum9Yk3H zR^o+~a&qIL{I~9XujZ@N zuO;j7IPR~c%TkomPK~uQJJN33TO3_X4>O=DC_zAOSLW5ROcWZWfGsT^0`(9Ec$!42cQlQFk9 zYmtE86~rAbJa)o07A4e_R-YYZ?Q51)HfKCeB@aL^hnp!MqGRiL+z&I?K-92kbJ=^c0NAX`1`#H#u8$Hg+lPsgN7j<|xqcLD|1 z*>2eTIk=8#SZ3!ZelS@UCDwZNS@BD>N>bJ_fjP_li-SHX&^}zn&Dmywo6D=_b`Ne2 zuC-ZyRBFro!>I|2XAII57CYKB2tM3QKWd$VSq1@!osCTFuoT{V?bh%#sJ39paXTGm`VtA zp<=HH5bpoB2I(chG@1$ z5KjWmBHB%n#6-4WBf}!v6Obw*JQ>g%0zhXvHK9%p;DpEc)u9*8c*?XbS6*xyLjB7; zNgdaHaDW`U1IR)-Xu7*)KHLbYoSk9u8Y0%17P`HnPp{w(*@iM0^kj&`9l4iJ?qE^q z3;r-%<^N&OlX^7Vl6;)@^b<%^^u?er`x+8_H1q*oE~^0=)>XkDp|0SI>H0d=6?@DV zu8P%^eKGVUza%5V&IjAW&vBEU67?$K4_m%|c~Q<6Q+IG-d(csHashh)E12uv5i6K` zSami&7QQATFZKobOK|muX}cE$yHWY;j=dfnI&|7|ptOTLz){0)7&T%vlcennm z&No6hd#MHYLhFVr%eOrBwEoykPw-RF4q|}Xhu9XW#NG~SV0TLfJ)7}F!3TO2j-R~E z$>x4@pDhlX)88Ax`6Dhr?B0FhcNV1<20O-O?UzonhkG(aSP!`ri198pl8$2@M+o&5 zJd)T84ZKL?9u2&aTx{*N(i=kRStHJ8Ej;mq4Bi7`H=7Ueb;g#?r2K8|E5*)P>&48B zdC`_}v*7-ZZgG3dM-E#B#Jqf1s)MA58*!f~w(g$)dje<$MD2SFA^( z9_8B!aSx~yVGEDkc`3gPE`|(`(4B6OO&2U*z1qd*u2+juj}Uc7k;-wI>C)JAq`Cs( z#+y6l?Buh%oOiC$m6)HgY-X9@$2c7T>M6g&`gsCh8(h`99S=~SPzUsGrFLvR z(r&C?B@fsw;w|VpR6THg)J^o2(chu44ZeoPHu~*ct@JiXJ23W$J5ct>J8;(sJCN5% zJFwS?JJ8q2JMj1jd=U6Zy&y7_9Tfb_@x2)kk5w}j*n^Db4@&G)1)6Wv$g4^;pXi})rD$H^e3`}9@c^#%K-me2 z<{K%-pD(IkXxpy27O1;V!y~>H>REqE{{W=%fb2y`^9Q^K z!VXyvrVW}tkUJy|kOP=Bn5VaRfYy3|#(EDzdk+%p%NOm-{~G&O!>fy*o9D1z#7yD| z8Fi<5l2RRI52Y(nNnV?fJ;4AeICsUhv_W1JHOKaN07y7EMfJ}cgwPhj;r&CN@;`s) zvG!LJAljIWqczC4Z)0Cjj_6;#fr(j~+L-*;9>5fpZDlNVluunjfkuoVQdps{bXG!* z(WN>Pt0harwv{k7q}n10P5S{A9U7YQ{hsm%r2cDS8c#EG5c9N5nY>%+Vqe7>XBRX! z^>S*)B-wSF#L@qQ5%(Tp^+QP7sCVlbphM1Sw>ck|p>h@BlA4K3y zW@kz1WTm2%Ih3{5{KFuzL+^b@w%+126z(M&LS=tw$KgRda`_x0i>{hAE4oT?$(&Yc z$-Gv9YGDhN!|~}pcbFohs&XBM9Z1d!R?Fsh+f3CS1$Ne;7cv)Nc6-Qb()c>b4Ck`M zl6x`BH-D{5%{cgD5T5aZ?2sl7^bqt0Irlp?EB=F}iPn&NhzEi|=w@%a{lcCul7U$* z^je8i!5=M8B-uej8?Qiq!rs4c2BusCE$%FQg`+|Ui-S-kQxO{FVWm%7TAD)^Tis*@ zK zJ_|Y&S5V$8O)c8UXl@H?pGHSmtIk?qmRd$>Pgmgzaf4U1c4rtqDFvm=kr3s-g5==~ z)R7(~<5{U#llj9Ff$2lI4@1N=LhqShJZ&H+lxN#TKW81^I_w`92p8Is2OjPRuE@tj zKlgbp!H;*Ni*!dAvGbjJn%8*Uh|71R;4Dg!S#@x#;jB3! zcQU5~5!?d9Yv0#x;3wqt3Ip%*t@WZAShYutYivQMXjOKTi z4#SlcX*9_rjsz$1ZJs&Mjp~)%;Nn5S_sHB3@9Pc^+;x|O zBVB4bKGe<6+0i?~E?4Vkm3tbYmyy2?hSr{8Q&np3I5QAh%}cvEoTTrf zMltqG$12exil;aLO)nC_HqLB#d1(~z%i9CTvk|%XcM#Qcc<%U9J#?qn0ii|90W zJlQ0$xkC}OVlP-cNp5|;=(>PkOkVAisf*{+>oF~?=Y#W1wz22Xq#A4u8F|Ub*%NXu zf_Cz$u}le-BY5EZC=QPf-xWJ}&9nGhLfv@d>O9uiS7wUei5Jt8A}8X;f4#KDh|G6x zM;_|9=U|RXPCxeWdNY0QIh@=7V+N^8eq)CT?91Z@?%Ox=f3<%7KMSD$x`@T9UMOR$ zqI_T`yu@8ZJ4cNASr?jw#AX+oA5Nsio8;X#(FL;xZ+BTgscbC@txetk6eqQO$s zPglPz)IpvbXP#J03mp+5;JQlXV8D|ohb^WB!6L#e!YtL4_p@GL*{bCCnM`#!j>I_^ zV=F6*Tk<4Mm^p{FsiRa|Bf^U4lw(4$?5%ZP1005W@1qE#Y ztSss^|E^MK7|iBKH6_Z(`0+;+5{HK>HG%#f3)H29>#-a`w&XB!5v?xuY;kA}DrV;< z2vv=da=oyCKuUr2WJXoJgM@LZ7j8leFiQn z-=&KJ=+z&2OUy9T%Ysgq(3K+!fZoqF@_NS60SHhOrFjHn;zMP|cY+Po=lKHxW-ZXx zBUgsxNZIBa$`;3|a0+TqLZ~=~h=r}Vfc{d`)}--U4U&Sr@&GQA?jxR4^;-x$R|1I2 zyT)ua1DeDrkw<1uF+1@y)-n_YM;vmTQ-EaPfoa8o47G<&f5J~wIpS)f7&~yeeV1N{ z&N6aJ{vIG~G2@Il4X(^6c_l7Twytya)qp6z2W|&2V4&7OH&AoU-_#z7rW%4FrUHV= zrV@fNre#5ja!zHciHF%o_b~8O80H>xP`L&kiTk)vxn>^Ws65jSRHE_Jv#bqsUHx9@ zXBb$Eu>}U^;siPomVB0-3ZOnL;t?u^cLl*6F()_{fcub@y0iJAr4WIpN7eUT`Ynbi z*Ib=Vq*EPq?c4}8*Mn4|PU_8e+!XF|!ET`xV-4vNqD3I^JJ*26xrJ%=BK)5qxu(qa z=P45J#>i3~*gey{19xoB7hb{GC9u~O6wFIIW!cK=kfcmhd<M_hhSCapmCjGE4O73HD@(JZEKse<*}J?70&>NJ87x}0Vj^I9FQLZiunj7TOVg_ z)`w`ajmiE%oyGbAbkVY!h1bDJWs^+0X!FHQ#I1D}$E3MHiL6(f9&^7kM$z1ld8oEw z@5|k|OFTCy(oPy4y>kK^L&9NYy-}EW>z_Jbf7a__WskZ&;ZQYE5G%?$M~erm4_@hA z&st0r2JxpAdk(%LXFl}2_cXbY+p?Q0R@t{@ZBApsS#j`$!{M5|W(f??@HC??&1HlV zO$#-)ok_h{tF+*JKo4}KasZQ=r-hBC2HPQ>E+J9a1j$*RYQlAj;aF(w>Avz+=vZmAxA1g^Ia$ij0AfhYu6Ht8QV=H%&~k>D zB#8N?ed%<}G$#O1t-I!+qx>e%EFyywW%c8&?kVyIPz^BHG=nVF8bD;yi&m*-MwpBm zVip<=Au_2Wsx;5h8Y3{Nj96+kIb_lDgUZwzy`-s-!KOiBsM2#L>lKDhppz60#W1NT zu+*t=Nv3IG6{s{VUW}F=VypDNt%h)EEIO79hSKO)pxqXF%f?GUB#GfG-42~+>EpAe zmEMMSbclT+X|e|pkI3BPixWxmToM zKS@td-@Jr7>^}5pq^uMht7D)qvW|4UM}d)tZB1>UC?aBKz$PVhH6hbf$(S)!BZQ*Xcl zkwZ>e8_e~w>CWUO@BZ~^seL;5%|D6tJPX8eR({EhyndbkvXb;SEdAdrNw%@Pa6tkn zAt#VfR)u=~YeI)BVG}~sNYKboUB}{$AZZ}EI5PlX4+`S^Frl&xqn}xxJofJ2tYsjj zbLz`yh@_lRp)_@e^hiB%%B36?wFm%2Ni8THOBEa;nn(?x!3=eq6KWShq*X%5Y;n8n z@j6sFUgWEvoTnMH(cgiM)%7?Agt+l(>La$;eG-bB6s%o|`dpB3;R$ATkt8H68 zd#+90VEphiGTdi7%wJ|XKYQZI^}0U=zrh+M2)GW_V<88}78KO4@2=UOCWsuW`RN&m z#I)Oq9_m2^ZqU>yu-LKKoyluB@|v)QKZ+0ucS+y0pArTkS2cuC^Ab9n?0d zXCm2}MVQQngcpMQv)Ll$j5Xq6SgFcXZiWu#F-)6mH5Zr$V}E(YBqFs`F(Z*d9S{Yo zo_B;PE|Gv)tDvw_McMQLon+@4>9qqvU*<3+Vqu!fntuusn;?$LnhiD2AX8N8ZXbr@ zH%aXQGJveD^%?6I1W3`W9gHCvCChq*<#7zo^;D!onOSHCo%pF3)c;f>I%tE-5ViP> z2qAQ9LJpo!1o-LM;{9fd6HOPi78pX<(D1qTCx^%A8G03Z_Qx?bGA==TX0m5*q|jQ zBsRrVgh=07FX+T;FAE2UMG|BNyIHT3@1)Iy#wOfpsxq1#QAPZxUa@B@{FjV={BX=z zfWS74hnW_6HTN79?&T55oI+N>Y}5u-WAh7kM+tC_)8rLSd6{no*;bJub!U0PvZ9w! zd&8?kj7p<=Kevg-ogjfie&`02UGbA|yMi$M7`A8w3AlxCpcA@9z#A$5??cx{DRk#= zB#9n(y z`cp-_tM3~V=*pW1_Ksdu+W{_&zrP=EcYCm7)0W>>WRN zCuR&M5xVZxQMf>u3zhr$pcP^91$%RRnK%8x@nVBDTVX5;1Zv??4O>|4X_x=QHU6D& zd-K~LMmqS%E(Sp~_LtR(zDdy383w3sV#yujbi8rLH$4}sRUo=?kY#4mK{tGsLr7{_ zJZk6!??6Gixhn6N0ftby&|V72V@25beaGKcLm1-@2U&KyaN>OUP4{1Frbqjqs+E}) zx>(evqB(fkDZNnT6URG!P}^ZnlJ(-5ZKd|n(2Zk-%bMm4yMVT1694ezRQjg_Z;}XL zufkwVDyoC+WAJA~%HDYV@ua9pzc&!@V}ShoT0pFmjQS z5#4|8CY$HGIdhg}$E6+r2g&9k=Uc`Q%a4tIoGNpufD){j5nI4pS9bx{Gq^V=tcsyod3WNq0?hIX+G(Wp>}2Uf|@>B$4& z&BxeTKe&YTGM|4X(xaYd*{hysxwGbPkvIIT>PsFk3)Pg5UXX7lI_kpAqkdirBH{yf z1}0ZvjVlSHq`ANyjY`t+jAx-QM((Q#QuRP3_<8G&+4lXrUZ)!44}#+sjbG_J0vXY} zEd!(VG#)?$ql{gJen8e<*P=HZQ5{TS&Q~Gg&7eW#j&>WfK;C)3#NVmZ1Eu3pp?VyoA8+;M@V`&vglpGN*hB(t&aT85-_z^I2(Tk+N}Ibb+6i4cE!lzP5W*EB zO;ym6sWEP?(+=52O!8ok-2cAC7wO)4!>}{#kLz=_zDLKk$-JxV%ATsydE#Ac_wFOV zNU}q`?A00PF?5@jn+wqG8F1|O4=!6!-r0kT*}saxS?8~;<0l)!;8Q)z=#P|=_bu@T zGET{5lqub`0xzmM^w^E3Oqpy@3C0~fx%tLCpZ-fxN>^i`lRttxV#Zw_}0RxB_O)v1j*`2@mi_&tUX}&*H#leaQ6hx5GzLrJJA$-grQ(#KJI`ydvAbv zj@H#tRJ9Z4BBV1*aM1vy52Y@?H`Aojd};+D0_HoGu~}LH2;{~)Q?1)2UZF0fE}9R> zNX}dk{2&J${pgMLuehqt`A!;jH)J$A^Thkt(ACam^SXY?rtBq+<~#ylC2$zqAt|sbhO*`wJ0DF_IBm=o;oho z6Pa`q0~>gQ*>45fH8Z?d7|pq=+nOz$Rc{8m6fq&xSY!9&&+qAFP9vg!y~?oFbwsSZ zR}Qx`9@|lVMD>{_{9^3vN--a0q#3Z~5yhIbfXC#*iK>H;*~=@kRrcsjBYofH(DRX) z56_b5{*j=@IPV<^L|KIpdBxu^+NX?k8++0*pw@f$3` z2=cz{8fBNFsAZ+jarJOz$8{dfmW4VyeU9~T8G=Z<&7&Z7QHjch&txd{tw6e4x8nsN z&n|HYgwGyr7y}X?-7ps|DDsZ}qQBYCOsR<@`Sq@ea{3-=v$J37kc1QGzREY^raogH zl)3c`8(OX3=Y;WX74sNGLewzde~ZVw#|ZerGeUr=|G#|TUlG&)h{WmoiWt>b#2Ej5 z#Dwh~EKQyM7OwyK_@86t8^;I*P(TndXC-4)d;oGoi`uIjmJ>t(h34+cs}BpQm-18T z&iqUJ(Z~o{#UXm#5M$@+(6b8|qevD5@?_#t6{Ey4A85sBse0~ZAH#X2?lP(lE@Dph zL9GI_YPQv^jrckVkwesQ@<#7`zfh}1JL{UTms5KM` zMIVAMKj1*c1?-5rNfsR^N$yF6um)-L(nlNh&8}aY1U~Rm~ko$+7$^M`_|U@KwsMcCt_cxnXwNoccwW z8YBV?FsOYE&oUA$mk^u#HW~S>{9-8Cro|62d{1PRR=T2nuU|e0ZbueiGe<5*#QzMdEfqUHfk? z-lr&qTan(ZQ4gt}9+=yF0t*-{hdAH4GX6hFjaohFLVxNZ?j=mX{M(8s(G9lotL zf=if+Tbslz&m~PS&%K7O5Ni}GlCl^&0JB^A$?9v@JBb+M+p)Iuv9~v0sz1cw?c0Iy z7tkiHW^Ic9K{yQepy=_Hc|V?6nPq7&d4yT~-W7()L#_hETYOJSnUGAnPqAj_eH{8LBOL4mpo|_1l=(PRByt2j)Of=a}a5!aY#JK!r!aO{`Cyu zj6ZW~eKlqLfcaWx{^K)5^1n(_s9CEctD|hAGsGJrh?b~Q7RItcraTM0&M6TF$-?I6 zTl@h?c1H#$LC(m4G%v5T{(k5Fvup&ay=wKYvU(nWe%|3u2G9Kc0hl?-duH1`&lfMv z|MT^p{u}UfD4~%Px#0YI1@wJ_7a9$jjs&C_b>-ncHsk23eoPACLhWOhG3=F?TC7~3gD8Ezc_Ad5F!(>ftY8^ddUj%~D4M4%B3%wCe+#x$~s9WhgJF_OlhU7KdW@I4y? z>6hy+&4=w7n(rt@4h5!1$=x$`HWvwNAwG|nHn1Ob6Ql#wm(1f0eGgLz-zg?~h^kHT59aoYK(Gl z4|E!%73!FXJ!gIxC(r#M5p&3UNGo?qec}l+q_@#?n}#->uzUAMpylYobWqy1A*e7$ zfeqp!WX)Kp5o2Fd)FKAz)F=j*&FW+J@co~{D0mPFXeFxFLTL(*R4u>}H07cji?Yk= zcYh=$Mt|9Ij8PG86d5*~iY1vQT5<=S`SZx!b_Z87lQ|^s1<5*?pxt?p-^p+Ap=*A?L zcp8;v`ZUB@NYwe}PGRogTueev985&5y+nw_NgPdps45~&;}3OvAH%G@d_Q9&*aqWi zL!g;=L8=x^mJ(*kV1Szlx|apTEtHP46)8OnP;WS5>OR&}wq~&DWDl9e^m?lxT*QFA zN)%P!$CdJ8;^pDxA@(U@tFobM5Z>U916_f#YoEqd3vRCigb#wr|=F<@G3ecgPzXy@?dOd-Vt%i$w zpPjn}5zeSpu9En`bw`}7-V>gX)_N=KEArpdp;lvjM;RDt0=t-Tdeq71p-oJcJ{_RF z>j;Q$7S84tv=I^6mj5ySFsgndc^sfRrDwHN+*K*bUMt`&m&9HCtotURPn0PAFeTM9 zOeraUHuEt`fnW^S?F;GvhrP*BeU?jXPvldt^#-xn>jw9j09JWj6;rOVpo&@sa-P3Z z7Mil!*H0k&UY3_n+!TX7JGXAiKO*##(C#hjTg{J;Dh;2AT+=PI6?&ZuTg#rFm^%E! z4`mAY1hmBENhXnL!nF^Qbz8t}Bdzusw*(Ggp^6uwkaHt!TH*8WM8N-Aw&j)DxjVm* zq1YEPr2RL`wyTZxUkI8+=>LBB&!{j*t_$i5L1#aHRcHeBQWb-B?nwts*fSZfErgr+TJJ zG3mTZKc4Ku!$a;n`Hvap7LN?GmJ(&h#(DpcB(+Y~_H&EkN>aJHN(2~k); zQ@E6P1}^{e&8kGAKnZ$68=uY0y47_hg`O6F7&t|W`mq3iF20gW4;+F7DWzn+iP?E3 z1NUWiSHJK3rD2+(cI1*YpNAj9L?AnY4(W~{njaUz!}%6^o-$?y7KZR4@h70?M^(Rf zT7?SSutv{RJ(u9-Bqj`Mh1@VF@CDLu&s2!8l%1N5aZMcG<6wnLW)*!2(#5pm6}$@$ zIpd0;_u57BpUFXsZ3X})LxUY5)E3R;`Z0rIb0)*JU+=TT<2`IBk^tb$IzCH4{Z1}d z3*Ka|QiWm40E~#23vJhy8(AnV&q!w^D+|q(0b5?rID>GvT|>Lm<{4V#ppmz=yUECt zIgVJF*W1{MVcsayY~E3zVFP)JM-nVJ5uWMPAw{(BtyUP2{P;?KU!e%Ttpagom>lp@ zBxls@>F?A#Q?8S(8F`xaX=)nZ#pzkf)+i*r0yqm33of>Zl8Iy746jG>$yq)O$bwHin#eqMf+}p zizBfe>Ue?&oyu|sZWL`|28q2MpM)F5)oJYn|36Na^_Yj>C0~TGp05(k|F31|e~LW+ z-_!FS=17XFtva?SiXR!xQ7A3_eo2@TRJs(B6wrniO>;_{0qL9;LQJ`9lZ+67iKDyV z-0t4>4Ye(Af5Yg8eePlWork#_;C+oVAk;d^1zH_x|q%o4WBYar&r7>QI-o)Rymq$o*s zE*fRHC*^;@t4V|YOB@Ea;kh#E!h;?J6cKEAhXyGj>v1N4qean9j>-T=8+6MBG zaf8g`yIl2Z=0O#T-EtVbXUHh;fogLBF9bfA%9e_unuFYc$@uRALUD_qznR=p7PA z6}HQCEy;wKXNU{!Q0^&)4^vvzGTxqp^gMPL(wcE=jWB10JmOHpdYwl}g*#{Nd^Xmo z?WZzIPpu#L{#5m+!ff7h%)Qj=vXtle9YdHUXs4=`D;7$t46kay6Gqz)d3x=RyZWF5 zu=E(7;YKjPs;sNVc?o&M>HAuw_$U#Pk}54gQwAhPl^wME=2KtyhzPLDWGPvFbq5N7OHlmuZ+v`|akmZ+4Q(B(R10NAs~r90z(Dr8!?UZt499!%f^uNsqFxftP+`ry&;H(EBAqwIRSqG z?*n$2-gn>$BctaT#zOZvs{Z%5U}ze%w%hNa;E4SQMn|NHEfRGon&Oc!7{5jn9Fk(X zVzenB1LJ!KF?{1H%iER|`owqf4~XHD1tXo(_|%Jhgm#~J6t6zvlfzZk+lP=S`hK_* z#|qo;&F1b60FxGy#NAR9!r&Wo6)L1SBbpvz$P-O4GM(JG=P;EM4^%$4p8**Om_@urL%i^ z2cG;Vc%T@^I&C^FZ3+`vkB;;(xY+1Ye5;5g;<~d6)UOa0sBQ2hd2?Q$Iul{@H4ra- z$HVLO3}5M+!`$TzP4tal)majbQGRoW=o#OeqN?DB;!Xrx56LHH$L}P?=LaM76m(Gj zeRcmY{E+`=Uu@y4Pf-W)+c$=PjUOa!ZCzaqjciQ+1|t4fFMsNqF51he+pDQHgcBsj z5fYGKA=L2`A_Q^{5jjH?t_KF>ayY^{Cnwaeq>Uy!-cCkR(vxecasuL9a|?ty*hoZO z#ggO{$uno>{+oHV%PoHt{Wh;&Yk8#bH1zzuItTe)bN>%x@8BG10B!q*lS$ICZQHhO z+qP}nwr$(C?PM~sJ$X6x?tAy1i&M9%|AFrMdVhQG_1kMT&2@;`eZRC=0cASq(`>!6tP(ISvTj#jg~5lT>?ys{W_ zIIX5d3!e_M__pE<;jm}u-)(f=txp!Ks z8{}&;IBv0$gIsaUw0)t~Z8NjZtV(An54BFflanbAK1;{eq`GYtzejg1TQt2YghD75 zSnJpx3k*2?Kn%gFJrYF+Q7X~=ddK~ILfl@0ZI)Z=U-vtMzdfS6;yf!E> zch$H&ewFUpKjQ}5p<6=b12_iNL!RI+WHAm)ycvv+7CmiDRse}#B%0CrW#TSZfslNE z!B(Yzi2gxye(qpT=Eap#8kn@rb_3i$(%GNep6lo|n)64Ss;23~Y(`Omo0RpN3sQ0G zj3zl+`&EOUCskMNRKuXx@Z~foGlvEAOINrk{W$>rQ6eOYP|^Sc943w1$1CV%L-wLg zik?)Xit*K_M3aTYA|#{fES~qvztBuPy2y>4hSn{q#xI35!5{Oq^*>SHyQg18sTY!^ zZ@wintMIV-LipS`Bqzi4iIWW`?Bw;9FOLJEnxKD<`}46|89_^VPb1tJH7;XC{yfn! z>T)WFEtHJo5Oh4p&Khe~f3!~q0d<~V5lDcvVvB1q@(|aP^*W-KB>f?erLY1>j?>GW zvp39Su|aQR)=?!q@vPd9+B>!0zd}BSzz<3+F6UyTT#Kh>$0jW)YN0)dQUqOX5NQ0y zmf;N#LtcA<-flcrKjyR8PAOX!3W zJQX5Chqa}&Bre90rKE}KS9B}|vxpUYj51@VE@VlW>)?fGEuyPvsrP{xajZqDp&L=c zz;H{KZ6xlF1ER6cu|v+xkHBCirsW6H45BX%nn+A`Xp}5M@6xSGG#p9QSHoN;xDCxb z#Mcs$|F$A(EZ2oOgVR#yu9M*`wT>!BhTZ{lV-6C%;wR;%FG<0ARAZBLo#4ynX15+< zw6d%Oi{=N6-5Qwe6X^mwJlQiQO+qyfOI>5(6oW}h7`xIL(JyQh4j8LU;Ld|-L z?kiVV+1Qq>w=_2#OFq6g!1G4NT~w9h2gsR2dMB>=s}}Uzl$1|1lpH@=c#GP2*|N&D zm2=0?PA_q=+<|+mn*2E<2b4r&PE|CN)@=dx_Ipj?G&Gq(9CIkecBrN*+GtBJlt`_l z-4+r)nUsbo)~I0(y;dS#0ei=g9XIH>c-d;c`-2P~`XzE@55h9c5;jk(2|CLka@{7bN=i`J zzM*zOOZ|gg+R~t65i}#@7g2krqOICE{(z6+;kr6HV2Cxc^`W-TEqzc%iJ+&nlZTdV zFPfXhA&-vcAfeN|mX5g!2S@`Kp%Qd`MWLn+G5+B zm80C$*1n;&&28rZWLLgB1**HhXd#nkzn0}L)p5~QX{CSqe#ez70V&8Ltg$5ig4tRk zjjaMls#2Dc5av{=iy|u6NyV#QL3dyrY7+uMi{qMaV%^~HxLwU@+nZ9+y82-(H#rAn z4x!@xFJRBrUb}qZJ*qr+I8?1W#qXG@dJKUC@s{i#)i=o2xI5Pw>md(#bP%>0lht1= zS|cSNA>4+k@*I&)g*nvt zw#KYEQ(~x(E~xIx$A30Ck()bDFiGYe_5Ru_z?A+f>n8)#iCu9;UJ|u2MK6UaL1{Im zSc2I;VgYgndj(YN^J0HK;Mi)co>$K9dPNa9lE*-0E(jBHMH9yENcWK6-x%i)qu%e) z72^&DwLEam3VTPn8x{15$>6kN&X$j|S#JwcBh5qVyrJUSltrl~eg2N<^%BYBgsbX$ zVQ>|ttOV33xp$JRBf~g8Z(tBXT`MW*F{h6`G?mN!8h-okDFFTiGY@~40+K#>NT55t zOWC0Dvza_9*;wZtROb%U^v2oak599w(j8?xufJILxAQRS+mD0h5jC4J-^cOB`uPr^ zuV}MJUBX&%eMereygI&UFP}hs#i+7J8lS)UN;$E)WApYwjy}mrZ=v=+b;EN@4+pvk z{9y^V=kWA&K5-s^7&;dMaW4({I^o937j&@)9RB6((`=4$mFE3{Lwq41gx$x;KI8y& zkFQ>nq#v^iPbHd=ghGWs@!c*f(FuBGM&-*JgAA*&3f zkbbW%EUn9C*j{D|p^7?}?fHs^fOi_cM0})N5)9haf_Ivqs^j+Ug{Nc(E%EMk#a#yb zdYH<{-aAqGy&p?;`=T>^VP|M_%%pI%P}`^b zjJ}rsAN5ub+5aDNF8`xxSQ5R;=Y$3V>iU7%$p80E!~Zue{WrnozZjZo)IGfa z181{k)EW)rAH)YH94eG`9l*58M@A$p4uwyG56?fEnwGlPzbBpb0v%Yjq@i80)X`e# zTvk0VMJX4eWUNNVQ+D@v#F*tcwT-+?=)FsEk@eHTDQWa#~BlUBLI`-wbY@`V6?a>Zg zp+HV|iPoQC^$DYuZ)~k#+Fb1|sHx~_;@WKKG%->Nq93QxUq`6t7hn@?# z(+la(w#OzfWYHVSUU?3k%Z+}KqyD~}M>5rR0|ZbJezP%=#bWoID=RHtYy1XR@ZZ1; zWG@Ks;Q%44{=-B~BtmpV1)4q;fLk=zT*%Oo)_*GQ2Ru^+HqQ!KPfo^B^}XOV>4=58MDF+zrq>~6oe=E?>RD*9|DMX& zxrY+agiEcYRuG&oMqm{>Wy}DXgi{KuMPmikTsr(j2P0%Pm>6}UH*>?4EXN8w ztBmf5V%#`HfkrfoRA<&B9f9hh4v>Ow)Ypbxa6#YS%EzPXZ^Wfm>;A3ynhv?eEqmcM zap$ZNSy^S69leX$u+EZBtWaPOk5{713DM_}hj?bO)FW*<#ER_5dpXvB_7W>X(c0^o`LPZ85Zf& zTR&LPX<>REwG@iE=jU%S%^9y{ti&9-Fx1oMDj+9I%(fEZ4MBtTD_eZ$;bMGaMhKLc z=@yRyDh~{c1JQ$X+J<9j9*M3DS8Z>X9E-{x)lZNDO~^t6e~$R7nRq4$U~XKA4tSz@ zb<~o z#?9}G35HEXWhJJP@`2k(y~8)wwl^`ZE^VzA?UT5xG##4y8zq(uCd2zwI9<2aCsv32 zH+VI2na3q0eZbnJDtMe{nQ4CMZ5%@uMh@k?mLs=Ef!sarGs3*I`|nEeyXgF?4E@+1 z|6+$@HtgnHNyDY5R-v)ZxHw5tVTE})0-ku)RXu1fVgF8^dMoo-%N&O0E0JBb+*^{D=vI<&n*W?u-#v)NJWGgP_)LlgY%AMD4+OGnRB%GL0%oB^5X7Y>ue z=-fC&B=iAt2vtzf;-djfrWcGyvVi5!i>8#3cohGu@M6aZEY4gsEY;F|d*lHU#QJ6# zg2j6R=e04Ekj*;xaO1f2--2356Df2SMU$H5yNb7r?OpWo{J9*7o2C2g&XWi1#s>G@ z@Qki1QJuv|48f^>pX~^eBcYyP`=9#h2Y&lZod^=89VpR-=Od8-rYH63`YU6~>N3Um zTKmEJ_w0YS{F|@{SA_u0Iby#6;()3L|6cQe;E&t3x9qc0Jsp=Ca=y^>w?d!dtB-gOhX@pN2r1y#Wdh0jX3g*GHJFP%J@>)BBABw2 zx+7N6)3E|Xrq7w8+b+ERYa}K?LD9YemE5^$XMm2#jyJknsfuT{vER0THbn7CQ9oml zKWV7U*mIdL>ry?GTImnw*aVIJ-9E{1{iIyJ$?V<>3%K(m3>n`tI`Z|Al-f2DBh!%!%$K+D?c=ZW)!snYw_1?d3s_f@>w=oU8XueYg`t(`{h%T+ihE z?QE|s_-l*%3R44psjJ_1lyrW->ecvLP6XH6&*FA%lrXsnw+( zVdQ_B?cZu|yUdB;+@$d`JGdfO?rBn}D|Ij?*_DtIT5gE!@n=tY zKzRg|A8umSR-9a(t55HwI<91FTY>vwbu4LKE8^o-*UlwBksF6na(^t~GqRVpSRTs` zS3)zHSJYyQdi~N_!N}ITAppYAR#!iR@u(8L9;&R2ciUKE7h)X>U;C)!Q6xT|$(N(q zQPuf7)vwqm`zy`Zo&rUucZ=$|_l@1_=4?;p2)(8V2d4SLQd@|Rs^J%2f6Jp8!s-#H zxBLMEbLf7&pwv&ZT;%^xs~wjuU;bcW@-KuT8nN!u`4@E|TOJjfgH3Qd&5&i`cE4ZD zurbVw`C-vHw)4YOyc~F~L?JkTwC%K^D_d^)Zk!h5D}4eb z_Q?KVk%RJY6MM0qw)$B$^)t9Pl0>XZL0`fRe5?p#{A0McLl=>P!Y+m!OFF7doX9M`(*wNS`Q;k2`+XsHNwq1& z7hKs-68@uqIJrgF3{aXAYtKmMT|k3S%+Jgr&i@vl#*ho4iLoWr&Hzqrf8D-qn?bv+ zQ|4uPqw0-kkbmn0v*!guKHl=6gb9Jr6+Erw$9c9Ls#*`>_MTiHMncf{ch!n?CAkMz z!QEjzBP+Ux3iHWLpt&dbCImyAe21Bw`zGf8GT%uE^Nu?{+6e zuqW`j?|_JTuaYzqMk&PGxS>U$#SooW9~|G9R`uq=C!59?c5C?}fKBnw<5yqm5XNWi zE8Jz3%jft$U9BR=ZJa@O0|Zks_FUj)JK3<{vcgYNdX->n?wGKETfy=wL#vstLLK7H zl~H|m8`{W|uiNI4>t!I~;DE4up&flE&7-Do|(!HWnuaDd?YK^5jB3QMi zvARNywL>LhZlkEXoGD*N;{&ME{ks*K=YQx5pMVBnHs0HE>ZztM7@7k zklG+0${|x4$Nhg+0vE+rl?Z+Sj>n(I{C~OA`EL@&j|%wzf?@wfKc1rcp@t-e{Eu{) zcvyu#QWXY{g>nvTd9_uZGFTr$nmS@VAC>I_el1j|v#Vh~&1e1Pq3=%})EQ7&Hm~mm z$5)Z3rx`0*0(X9hldGw1@5hYW%{Et;-}l4C4N&20M#0X$?6hvQ#am!*)JePLdV_=K z=*-U!%9lQLHS8}Di>iE#H+72FILlZZX=o}GB_$Bx%&jC+`{Y-f@))rqo{*Er#C%(q zkqPIF+LyU0d;QRGy&q?SPC9m!nPQVhXh6pI0GqVpVa2hwA!|~)34803T1%Q?#)^q- z`PWfI!?7T@i;?7hH*-PFrrRLP(qTh$E}L?hIV721M5ci_Re388f%~v-w79!4WfZgG zsJ(e<;#<2t*(lR@K-d6VAyy~T6LbKO$nC>DtDB$ zAa`)E%W|Nk(qP%!5s4=AZ8`PSKPzkKot5K>rALIS%)OU?N)N^^00NS`F1A~iy1BM; zHS*v_=x~QnhX9)=kc5)V--gIZfZP;<(501zl}jG#_#i-mChuagC8k6q02Yz3RZT~E z2LOePZ5|3S0ksg1b;+)IkwTqx2$t*TA?J8jl7t+GmRfFL;dNPFb&a6dmVh*q!F zu$Oo`Oy?0Nv_D)mex`Q+_hzueK#xMvv!U`eN{WLh)(D1w?D;g_Q zvn?MpCpM@lyp+1Do>vSKvcHO&*jx9fJxIH-inbdW-!4&#Q$1H@UPk zGQMqHmp6$Jn|mj;ULfv+_VT3N*xoAO%X6MGK7J8(!qID~l`I0cOKN*Yy}X`xA$Xv05Zj^FAKZe+>(yF`RcKJGP&53;#YXipAiO32!Z4 z2LHj~XiXKhWZ=L>HL~J=ghuZr$x_FusfI7c=wUv8!>OTN95>%y>xw{{Gd~>oK{>RE zWC^8@;1*|u=I9|iqD?E`Vi5zB5Ycu+`MM%zkNIzY%>o{6_P6=nL3=>4(A_mpuUr}) zeO5_Ff)SDyhppj-f)N+#;;TtJE)*8=drv`vx%k834Pp-h(+&`IlM58wlG5Z8iSY^; zmb*d{%9ys$XA&bLWVFA8uJ=i5sYr0dEFKk-?er!8p$@W0rx89$NSSeXFtzJ$5>WdQ zl(G~HOJ8z$w@3E{uvy)mkG=wIR}bx2M*CFa{CXMwx$^w$g1JpTZ<`vq3syb6nZ>;R z3Q3i>g??$eXn%qBHr@#QM;3S?96v;&dRc(f9d)%tH~^5=?8sp>d`x%M94!1$Mm*B=h!p>FKVgt@V&-`v$p72cydTR(}AOm&IZQM34`S~VY zwmf)u)uDW90e9$5j6PsPo;>tzl{~^wCRCf85BisDzq~ssPem@1a*P|~?1}E)kFw$U zRk}fdICS}EKHqBGKvoQriD_Iv*T7)-BQb2ufe3neTs=;pKg<9jN;y1?21AjH3n@gG zi>$CJ^He({)|aMe=GZy%bmFG zAi}k1Jd}<&lI^*sFlGaDu|BWB&yeXJ6|6MISbVwqt%f4;DFA@%c!g8IkW`HDr{W7Y z#;fy=-$gRDEzBdN#`!g^!EDdB3wcG`cJ^W(laji3VjBi==Wa)cM5E(*Xm9^F9KUB_ z^B?}7NS|{k>qT~s_iM(-dFQ)`XGvaYLBrH)8M_Omz?ar4^j-ZdQxTV>QkGz`nq71s0nInv{ZMp@u+JE&Xj|voDsf zmM?+K<qK8f498Q-*R+mW3z;Y*Br8Cwx4mP)o>yKW#r-xmsEM^#*e+%6& zAH6p|y=!W_zV94BPlmDmkP2K2`_Ih5UPQ8LLTNT4JE2Q&TZmZpm+ns)uFJ7n5$f9z z1n`&v*s&v5p(Z-Egtj3_Gze2JPW4gXlTF@#+cbkx`G*n~)#_qMjtsWK5Caf<(AZ6- zReWxtU&A9+XFOgRR4*xV@Gh!DFnY06nAwXz9Uo7)8m zriMXM?OKt6MHgGO4HpKvavLa_EDSV_$SzAzqmaX{SkueLLh(h~H}`+ed#MUBrUzC6 zai^2+OFVMa^2=-?gl0#+>u@m~|0@zKqOREJCqD!BNf+rd*YT>}IwufP%UHD3hphxn zk2wOXY-D!mA~Os_f)QHh&BmAi+g_EVgFq%-B2>eI^UG2mi~qL^2ydgV#S>MCr6aRu zRyfzfuE;N0;U46re*aEP7nTrqE-*@=qPvo)(*g>9ygXxlLQ)C4J~^#kS&@l;V;G*0 zQN&JP;c@N)*oi}OWy$0bWWhwHOI#|91p$jFjUB`V>;juvgdOexW)67JvIAv{A;wZ6 zc3Uk-O)e^C+5K@c{g9#73sp8&*2eWwB6JOq$8E>z-}wGa;DHKWURKL8>IxWdA>f~^ zn2QNkdzG~{(UcyohnM)1_ObnU1q)~35}Nb0Os*skrdicP5=9B-=t@l~|m)(DV z4pXm=VPY@FjZ-@`IiO5-W`SKZTsUiv!}x6ixbbGR4i(p6*3tVttr621I@wX~?tRG; z;yrc5BwCrnX;a#X7mij#y&7$TrU=Fx31ey4PITC4{C-fVH1Ea#c7kZkJoAK98 zUN{$K>z}7f<$takUr|oGPUvq3Lzbd=E#ipiZJM7JZ=WE^24m}G@(DAeg*k^9NL9Eo zRAD@23qn&bm`IB77JvxDO>~xsb)2kwt&pEI%h)VQ5*^O-q2beD3n2hkalTv&XcwsK z=$1H1kM_Le#PuYrHgFbJ_z?o60=oGP|LRns!uEJq~TTeHvh2a+=}4Vn3lg*S-Y4Jp|ucBRR`qT zCWP*LfZ2Z4L*Sh*hu3?#Hkw^j3~YB`w2e1uZOjh&xVmetJ6Z^<1gI8(N+-tc7bPCM zswPO~0jL;y?KQf%k~i$HTN;0-gM8$jsd*2J`Q01BE&wy zsY8}g2`Wqp9z7O0#GdF}cfICfyY7`ybtZwz{PHEoTE_hhe?w6#ILB@#l=yW<<8y07 zX&~0c1hd^Q$MR}MZ3U@Hz<3J{xf7^&k2w=StSNC^Ia@jQzC83Au6Y|eGMLTaiV5l7 zHr1G*<~5SaWR7hVv6&@+5`^e;i501=zSGh&2>c~Jk0-)7%QTjimTVN4$V92~WFi)q zopCgSM%X1ZzDrI=r9HJJIt_6M)>3QJsg@aLlhRm4iNutCLX3%636sP9p4mPA18)xD zl5{hE?U>Y3C8fW$6XIUbebS%P2g@bj!_vG`QeQY6+7f!@@zl=4)Xndxpj&4aO7rF< zJBlvvCJdD=PuQY`5oNe-VW-5cI199FDb;Ap*KB7w?Xd=XDuE3vcw(=1@jO8_=^Ct> zF-VOdzx|e{eqhGy3`e&{TZJK!STxnNUw5c9OpDVgl-o{89` zN^(p!#s~ho)H)c4D`J_1E;Y36N*_6V0{W~z{PHH^g6kp=^z4uvOsveA&9anKN9@8RgI12tC0@B#~F z)nOxT;A^v^q#l35A#!AqYf0vU9Qi?9@_nAXIQZpg*&e7U%d1EMJ(kGGAty;yl0Dw`=fj5Vzz?Je#QR?=I%-wzYh|Aij_N2~OB%E(eHli6&eUDX8yu6iYXF&9$p;enptm75 zL@ja`sAn{PC;tFnQp2#eWGxRAc!O}TL&zPgJ!NdV6xlXPExF~#gkzD5FUw2qa;y=m z)5sk(q9~5c%A{(Bg^;OJlZan@GmsTO(CC4`dqd`(i;HB|JuHmSlRcO!Ws6Z;dKHD* zOB|!{7Q^G~f@7LtToONm@L~|z{5DN3;od_j_CuWt${Wwi%VK^~+qjfwNNdxF^^Mu< zr#q#O^Nrdo5KO*p{qp(*MiK_UdG|Z6%_j2T`xnf|1o1r)b*0x%HH76ZvGB_T7I4Du z<~OpJPF8X#@R{=&IeusR8PsK{9HAxgO=tQC(E<2h%s2gqS~%|~+P4vH9b(QapBpdl zAltr8_mK7;jb-qsUrC-_#71Fwm;dC%8hu{h8A4H#&xQ9h9kkOzn1_8mnAQe(s{x8$ zb{4z{I>Y6@CnR2C7sA++y#qAj`jpx(5BhM>dTdxk10L5M0MCGKpRhYR>W-2^_sr=+ zMqBiQuIyAzzDwtE&ero(k^x0+!AdPkQ=16$4Vx2VEuv)k0s9ic${w_)L&5sQ=@ev} z*md8u0jn+C>MghfvMplEfug5TZ99%@Sg2izJFrXLtXs~c^0ir7jNz|hVR5P-1%f+! zohA$lM0rkh%=kIv%OFwU;JCe(czW^eYRO3OXWso-)(fWWPtdw6uA_EBjhGCbgtBrb6F!)z?l{l+E91IrF0(E<8v{MQ{l~g1TeR|; z*7t(cc24EvSE#ca3;;@-eU_^T5**5Fkuwn%?9{D32P0|~I~Lu^;QfccrB zRMKL`ohD+@(o8dyuBMBbKYP*KCkp#JzxZy+oO1|Dy9wy^)7Yqy84L;I*l5(*`GT=& z)!jjbHa)ijBLZ7#A&*Y>!(K=^5>l4#s;*k53~d zT?b_XsGb#O#cke*T^Q*)><(HZ5O-11FL_Q{0AJCYMxZCr+*_U4)*&F!&lTGxuq{W|HiYsgb291hW-;?e$W?Ks{xDp-aW#%R|&@*icOJCxq9jX$bFE)3b>QC5??GBNS7TOn7%mLB99N9w2bcWA+Wp` z`Vw+nub(RIT^KTK*{!|(j+pkidWTenk>o|8D5OpiWA%|_(t=Z&`Skr|eG+o~dcPrP zo}b{hzmbV_D&St%yoWR{J>!C@j}C}1(3X;-4DbtfNUyh$ZT7kT$s9UWsDDEL=Q%d8 z`)k$bXE)vXW3B(ccpCoq{q%o7()};~zCRX7>e~NdfkaO~bju8hBp)sz-p>xu64xdi z5C$PO^v8!sjOOGGg^*^P-k**i>8aUy*{od9bUWI_Tx?9ww5+OWajnz2G_|R6-E-6W z*z+}aF{pvtIv0DzMXo8!ktm5lj17RsY&V~0m?Mg(2Z4R|=) z`P&|x;5ey3aH7m;s(4zR7s)zAW>zEGvg{sX^Vp*0V?dRz_%3~T*@nmn)vAsN>eH~w zHuX}{<(w9q(W>q|9!UbrtbV6Y%}l&wyKkL+G@zkQRFlZjR-QQIwY1O(ZNWyQDSMSs1|&>-ee@AQQhZH3>{w1~EBW&5 z7g_-igv}rP=#3**QJ_m4>a0@ z2U?OGv(X@E;9t0JIrKsauc{aR{JD0|xQdw(xE1>GuHhO%3q7g8&?}Bdya6h%QJ3}V z=gR}O%$d_Q9Qq;pa8NOJFMCC@GD=$saebBED$t}t;VTrZZoqGzYN2t zS|CNJdd^mh^B#Oysfc5H*2&f`&n)OgqL8CZ{vxpZl)iF2cOt4y^wgq`w-WYneqpq$ zie0)awkd}dr}t>~BP8m!9U-le{1R*yba?1C!zPRB0hv0JI0h^4Z=vL#4PJCypl(Yj zW4MSskdH13R5K9a{9AFkKAM#mc%v=dnGtKPb{HALZa7UDWM^ zWb?XZ*dA0je1xuUBDjTAd{^BjrTB8Fl+rYVwV;VSm9o#qT+rk^Dxji4S(f>AJ<8+Q z=2am?sEHPp0g%{Z#x1R1f5d3V3o}Ob=nuidAQnpESnCJzf#(?%6v%6?FftFTJ8wUm z)-JQE*f~eJYEf)vyp`~lY`Vv=-c7ex+Z54kq*}@~S>Bb57V<;Qac-9XU7Ylbg?Di0VZY&|~VJZgJ(L`I-cR zi;=J?{foD*tB;>gtEq5A^iA95hK`|Rb8{|)&Z_XQ_dy$n@fm%0K;6CvT<(yXi8QSG z0FMBZEF#+Gvf-wZ^2^0*Xz;hpR;Abqz?66ZWPj^8h8i_Vq&xu4c;XqyTP=Ud4P?St zG=9#AR0qd*4;0O}zlf>!kO_VdO%eo^#*O&B-C3y~FV4*q``Zn8Y=|MZhbQ zlFSV~(x6_8svpr@Sm+TbfHB2p){{vz59A&1wXFH14?w-x`UxjT)3wfRMw>Z{)%ZQ~ zO?=3fj+)+*begMLFO6F*_#1*O3IG~~f}2n=1==zVPs?-38ebR2gL^E#)@2oRa7VxaSp73 zqj~S|ib9uYHt~#xX_4k}dGcLsYJTjSzAq2_lf4rEQPlT8Zs|m1Y5|im=qX zb5pl?pVUraxJoi&-tGiE)syH*Hx+5xq8n4H?8c$gAoYaF~&Rm6NzHx(tG9rerY6 z8P4heG)sCl)JaZejbKK?au!67C!J)kQ<9zCeGmKFtW@vA3Kv$jsRLy&J2X5UJ%QKy zM`fQiK2>UF~kOxafqf4)~3C>9AH3Rc< zJCrtf22%SxFl(bS(VPdo7nC-VIZ8Tz8GlSyKTkjuIBcO%D=m^cV%nPV8Ar|n> zbl@fABPV-y-r2NqS|2{uh6`2d1WGl`WAzT~<$O@1%bysCFBI?$qww1lmQbos@LC4I zTKgCHH3Rm3nz!B&-pEr}MWs;o%o%)Ae@gj~)~#x0F&f&DO*^oy1yncA4UqChjyu{6 ziPp_2cbH`l#EnR(We?mO*d0M@V?6@g{*ZNtd%ou#!uIbr_Ask= zdHnC_(T=JOJsGj;0mkYP$z98-j$Aj3TAeUxds@243y-L&cL%VC29x*#H-)*dB|5xMAXSQN z4Rpd>W3+>m&WJE{dJ@cAuOwIP#nKExb01*2{U=E7^8&ow!{13j_fN~s>m=^!gTaWY zCD=_#OsY%qo)6@}Qz#?8XTNb#ev+QGStA!3qZPPyM{^ahk6x5Md3@YM;}47B;dmYa zz3Iq}(YRo=1~(;uuBkswK%nQ2&smQeaBBCJnaI9mPpPi8DT8?_gIZL@`%u5;*F^7X zpvpec2VRV~^khRh;|5o*=<3jiqK;lAMwpd-d(4`F5A;nTd-1*o8adp9iB1W8+lceA36{ zZ`~r0J`?}%hLH1%7KP6eZ)88ZGRp7NJtB7><$Ug!6orH1{-57KrbQ%Gxhq7k$da0i zg|$L1AvJNeOlpL&Zpd6&(|p$s-*w-W^bZ%iBU#dg&hnvcbw3aJP*3rhPxQ?P#uA)j zQW$y@9Fr)XfirjHRW~=sF&;!>rcTbIR0LQ$(+J&3v!85RU$vn$x>J1#aZHo230{FF zVnegn0dm)cIM4Jhf+1hv$u#LmH+;v4#JPT_36tPlvwV65j04SHxMny~Q$E^GxY(QynfTAVUqp!pT7*vIOmHGmz zrU+BtWR+ zPo}0hmqunU{(Q-kN)mO zISF^ML|O0Mh3V;wh~|#daF#+nTUmdJ0xp{wXZUWpL150hiugfDTM|EtG;E7?Ld*yV zZsunDowl8MgpR(jfIDguuv8;Qu*P!#JbjW$nt4m-LG?}gZ2B3WQ!jT&#plvPv?9j_Q5q!Z^TBl@#b9O) z#qe76J^+}bH+%xQ_JAY+$p(h0Wvnj zBi#~Ix36-w4^ZM&PXjDpIOIEmK;X!srWNZI&So!+I=J=9p#NbaS9j@6tGvTbawI<@4i%Su$O&=;SJWz+wO)q@Pn zO8<@|+Pu%k&9Z4?nFFoI%AJ1|?PUTG7j_&byRv1mTS}W2iSh0B&te7+X<(u4MhRD$duN8M$2@No1 zxf|8IC#LWsd_wog_Oklui)aXyR@(=;2<^Ja2%+v;#_g)h&4ZhbstZ$7CxRun?0V~z zXlzo>MLC5mpQ5FopYe2zfYYb@XW3rujp-V+dy~+JCHbW%chR=Lu?WmO7Vscc9yS_l z(3~X=buNojTcI~dlnt8H=2R(*_o4ytWI&$#8nQX?lD;xjr^{Djgcazu^wF7v>L}_e z`YZLS0JT|rdonPk>PkH&lj_RqRL@FHKmy5BLtq%-t0F@Oyr@03ABfiw{dcZ9?~u(^ zVPsNU__+ho{xNHb%ZsB!saguZa(nnY_wOQfj;YwejA5F99K-fZ&LFV?c3xDVl*1ay zQe(PE$111JPA>z>oPz|32}DNepFme;C()U(PGyDd5^SqlHAYPBf{=8XeOyKW8&`iK zRRL9Z>tEKKpun?;ft<3Lb3&>PfpB-z`=NeE3#aQP>=lpt0iV6{hik+Z5S3dBUa5Ux z-kZw%4`Jh_K52bmJ%&e~EHs+F@s~n=(}^B~%wwGyQ;Brqm+v+#C|-X=F}&0thxNf* z&o%?EZ{*EOx*n!0sRnjI}|=2|3mP@aPGU>g~RG4OnA7sbcnHu#qEh zyAzM+Q@tgqjg0oQ{sBb-O|E-}@{o&1=ARDGLZ=q14#2uXcZ!`4-<`FZ=lTSlquI1Bzz1->2G629 zl+X)32fD$3J<24wh9*iV)hp`^hHZZpn7pHJplxzdUn1P0iK%}|jk570@UkBe2|Can zSb3xk3sFj-dkI^XZl1jU+H2*Gp29Np49N~p%ju76x$~%gJXD(-t2^k@{e0(epZxrA z#TN35Svj6%c-AFiyM?&Edg>eew}&qI&pR55;>Axam>EpVZ{rm9f zDY*|FhxWrBl4V1L6aqi=^?zzxx-DwA?S9gs;*o%W@c;MN)_v@W+0u9yPXX%eCT34to$C_RqZ5&vB@#%ixFqyNX8(JsBkN4T#nb=`j?RL+@*b(ajV15ph-0;V z5eP4^ndXukC0g2SnB&5=VXNz0i9S5E<=PogfcnI zXDdzC<585SPc@i3k3RnD6uBw z`A*brGg6tS8G=LN($GwNt0=uVk!4I1#)KtSL|#dbRfQ4ma~a{O$#k;vL$nlaWQpMy z2j<*0fzG?+ol(HzdJrY+QH~c?+o$kvyRkTlH?xJ!XJllqknN7Ix{rok5IpNLUEBKx zLwst=b0)8ih2Zbg4z-RD*U3#n)2H`0vH{1N z>lDziW7<~nNufvSE;6LpgdGIyGz4+85ei_bWO}JnBf)0pM_n13#j_Cx?6hX}h3p0A zId{dp`2I^y#ZC7^k@_aC>uo~ z2t;Fd%OLAbi`iVBg1mY1yeQ17j_JNfPFP#pk zyc_X@i-7`egs4W?^p=aV-FefDcXz|$&div&u@;rmt7#mni!X;kukl!T{a?D$`eBdz zM3JhA3~lVnqN1tXDDTu@RFNQs{GqH-&_`T~6}Q6VnWpnTf#WhhqB)<%Sa1hQ+&ECF zp$lg1eg_E%bhxR3zNmRd$1_h=^|Aa`OP^r1LsL;=x8UcOMZ|;M5@>d%(-^o}1%{&7 zj^~g?tf{6Q8VnQl&emq8&dNr&ej71Pj2Sow#)?sKzn58E(umMkk6(~`3CX-w@=t6m z%{GSXCL;9A_FI;_=rus-EkYdV>FTIgF^1qlP8Z)f!Lz% z`qJdU#yJF>k(^jeCnry2uTk#%9*-jH<|~<{=Tu3}wKs~`rlej((UHGh>CWfKhy6c{ zy>oEn@3u8Q6Qg5mV%xTDOl;ejBoo`VZ95Y?nb@|i3Ew{F{O1CN>x{N zRqx)tpSAb1_FCn(YNO%3VH^$GF=Ch+8&pLV!eJ5|t{j<4f?f)2Osn}0HU!Lkl|>Us zPx?d&;pSb6D>sjW9?sN!nyby>q{9&90PY{xJ>yR)2M5rw%X%CodXn7V@&uA#7c?er z)H&$9q+xg~x8V#alk0qwy1q^|Tz}^c2q#u9g$957UI{R+JI(Lw9CA%FFwpM@XICrm zyYn5}6dH+GM~}88&5^^oifV++v8qkhv%aM>u5OBx?=9Kj4{}Mlz1B5Clr=w8_HSSC zm$$!7fXST#J7yhiC;gd2@}43k(JqVnhwNmE)FWEs=;Tcq_u$6R&~}dXPGrpZZkaT_oe}iQWWR;vEfglYSlTU=faTxIu7~W6 zS&NSItO0Zozirf|4m-vUKQn3NC7eE7=a3MLs<|1Wc+s^fZi=;QK$huO9uIoIsd<1n zOVnE2!``>Z7%6NsvONEB)gXv)g9EGX#p;BSCrBNf8P;#nNzL@`*VI^)k4*wRx;ZgY zn^+BbUF(V#5z7||+oTE>1sng=W9;4B?$o=>IcL|1*>YU-5r@^~lkv=u$=iV7Rhx$; zwQL)of8kw&ddz^zkPQSE!SukDTO+YvREh;RA+W1oxZT+_k<#rp3Er&kOsHn+`W$dY zH6oh@MU^_m+aJn8q)_V|Lj2K#4*)AcYEpRV2q823RDe<|bHY1mrq1e;iOHPm)#a>rrh5rSHArIA4VI!`L}aNsmLJ zhdnyb;1h%JC)?Z1HC(r_x>+fXYZRD#c(n$k%XMHE;$*?+ykqn54YR7q=Fgfvabk*% zIXk+JsVztGlFSbBBwVhv2Enz4*tz1GOv>F%_P)VWrkJRvj&$(&H7a$bbDcSFmcp!Q ztoiveKz>M$ehk)p4oyBIsO|g$RDL=XwbbG?9Pjx)`WW;o8i-0Q=fXnD!IIxF{SoLK zK`Tn^TCBd6vG{$J;k>4XW8ADkp2qk+^Km6ITH4a_Io=eT!NEjy- zkdbtyTOPZ%syinS>$(#eu5kk7-&^#7MgX^vG=6f`A;RS&a}N-GQ!}XBmZZh5{a%@e zXXi}Tua^plncT)z<-vzh$(r0f$#Z06KNy_YP0JHlu$iUNhk2b)n3}*V{SZUnB70Q_ zRehq&py^gV7hzsZQ+lPampe7`C=0pP`SClkJ*)RCQsI16DXLAPCvW;7L}QriIuYBh zYh76Hrk}SVGqm-N{EnKg6W#5Y0b>p8P?_$(C3!{ouiA_q9x?QxYCQa>5i7hB%25>Vd11b1&)MdUX4f* zLTo0F@C|)A&1cyC>8`d2Yi|}=BTJ351V+<=#{@?it8gejn6mazD9Cf3>3+9$HbeSF z%*s~N(WYzrD+)(0hD(KB5B}MxNG)LvXklT9gC$#y| zj>~C3EYj#lGF=vDVP*CRMx8(vR3C|c7f>Jpb`>x|wgyj3?)+}6<@4KaAkGM7^XLWibH4OTN$QlvDVtG5#Eg8~EZAv>hg2n_VOB}uFb=gMLPZtK8FC$+@@xd2 zu11-sM)-jgO_av>#Zm-rok|^Tyr~?*7hEs=r5zr9X_8;B*Lhwa>O|$~R}tJ_AhY+F z-BJ$IH3#BQA~=$_`G$UpT;XHd4%q(~29bLYx~}hN1>I}c3ZQ`HmLkVn4rMM3ugXjZSUBz5Fuk;!{mRDU? zUp=jdZr}cb6~rzV1;S^)+VA%6O=@3<>cgdMgbdvwt1atTb@MQfI;7^si(Ajo@f-L< z&aWpdn2#w@%<6fSuaspc5~|iUx`t0YG)=15<94t~&FM|noVzrp=gshp%^9sG2%6Xp zsZQS`m=UJQL@ubt*2+%$Y}l6YK7888*Pi);Z@#GC#E!HuDt(hRIJ%PNsWu!}f4m?z z9WoiWND{xlGx6wrTB`%m(j(w_1bg}z8e4>ncs4z!ynGM-LTa53&f^&luNp6vYXtT= z>N*pe(`KvBCD<3VT*OyAKe1@O(@uo=|GGKdnL%lX!m4cqtt>ew64rdJbEC}07<~Xv!D+oY)D2_Us6Z~Sy(GVM-YTf`VFI|~p zUn=AI^0%Y$KeVi~ceHc&_)jeBAds*9IDiMN&wsa2{Ev4LwKX@eH3Ir6u>Hl9BumZ8 z?r&VnJ(oS>4vU0obzNC0*}AM&IvY*%x#VU!Y?+HfmKY)T)CbdD>-tUBj#sLhA1J$1t6?bIE)?~yuBKP(%tzT?W<$R3cdz(gqIY?NZ4){e z&De@f;CNBS*A{0Tt(P;EWuw-5mA^6sCwsUkMq!nwMMaq$wjDwcFy7nkV5-9r7g)j) zukSaXYPvGmrqSsU#oz%Zty;yw352hI5r9pPAX4-*oa?AW<(iy*-%yE4WG(o8y6%eU zO3oV(uFh^q$;v-ct2{A3iPb$Yx=ADEF+QWS!DO+%J#`{?jX9W8N&KakC38V`gZFnV z%tF@pCZBf$_i#a&pc2%^T{?*^Onx}vktX1J1W6~rX8J4yNc+D?v)W?H_SRqKp%fhWjO;{W%MKPu; zJjuJ4nXCy6W>vlR{`Iwz!C!<(K=T-Jp-C$utQoXlB?ZB}xsk;9g{=nDb9YBzvc<8m zUFNcsqClQde)+LR!L^Y$vY`(huVk zq2si^FR)y4^r8%x_R(1@bFA%Bazrr;{V*JImp!%8#&kU1n%AY6|k$$lC5W^?*bqUofA6gMAD<6{f-*GQkqYEHh^(Xw2v~x}eGpyI7RjHpn|g zk$&T~@E zpH$Vzxoh@ks7bRBOse{La+b{+WH2B*5wQrOer-KUFa^j^4of=`b%3XI zuQqni9vClU!z0Sd^BnIEO&PdPDWhR+uh8zuZObI$w&x~?$Po*FuvKqD9e%DqnQ~BH z3XK>+7S@`bRWXuka$z;PhMoysQ(^yOPMEUe`X_?N7R@?JZ*fpSGS!*eYk55{qgR|A z6R+mFr7>(g;>B)u3wFBHW6pf37)i(7iWuzJLKG)zD}NJ_@G{p;d|&-M>b^?fx6l{- zBJLQ5i}>OL&G}PC4S%y(jbK&=R|KKUZhDTOTOvnE+D#4XhNEk+b7+KqS#N;1rTh)Y z>TRp!5$clm6=BXgXItXq!uuEDX@5>An!|31ORjejvgI^)cK4|-QDgk6xy_MHa{(Gy}8N&JKhBD$ow z8l_2RiL}Br7|XM!KduHNVl^0dj%#wnpNfQi)`i< zy`d+X{MLX#W9SOAkL82;nh=s2i)R-a``WuG$L25*`pNg=qoC5~7pX=ev!6X)Ga>3X z*g=Bhl_=6XE}c7Jv9}a2p_`%Pk^6B#SormXnMER}K==iuYc>T+N{^7pJ1Sw8orBgp z!fDp6mBKs5-~>`T&W9w@tuqPKJ!RA8fTka|3))e5?$uXdASae1;i|C2Wcg0l`FCQD#t4k{M@a>3)1}nsT5W6kF)_lKs8gUIG7}R=krjbS~nOU<7W$m{I{;$-&a2d zALuPs3utBh@7a&>Z!)Bbxf|~&e#b25{wXop@BtHFl%L?V*SmPCHl7>;V?-7uwKed0P|h<(~k}OFGBZF!fHhf z+;OIDo8d+dt(yZ%daN0mFMMVK@y;eSbFJhQ)#GJLzmyqJu z-*pVxiuP5Iiuc?Tew>`uPiKl|xW~VViK%YC*i$L7E_oaP3{6!h9D$C=Y}5Wh?IT3a zJ9(SWQLcz88=$HUpi8b>{~VH#GclHGU`U>T&%f!Y{39eX2A&==b_T{Kj(-7rY+@#1 zfPTk8zgexueDYjpy4j{c8zVKLX+8ZN(nWAo=7qc((Rl?B(4xSgXLkGkz-J;{-nsaG zv1cTrF;u;N8!*Wg&MEn>q_&RIGA2EsXaW#P71^a~MR(|V@+ma#ohF+21h5NP>5VW8 zXD|T$cWnqbR;R@UQ)$&VYusg1**2OJsc8F1OzkJ0Bij6g9Y+gSgEU>B(5(@<4oT~w zb?yOhfCZba^=V_+bGUzw!SY<gZr0n zXZKfgg9L-M(o}I!x)84GKl8x|F>mXUfpKG(_+0vcrn*)PZCK|0r4NnKWx;HNtCXSW z=~ax|OQPwzdiZcal3krzexCvtybNLv!#>WWE*7kYv*OQjg;1aZOYPpfRjDkDkQ8gw zsAqo$cY|f_DE2Rv8;bLm3>~V>LeBohEf%*mBKFg!rxye_ zmb4aScAACkC!?!9(fCiOFkzJXWc7Oet}_Yh+JtWDghokx$>V_L`?%(5c9EYKL*P>Q zc}4H|FLhflZ8@viX-RR$Rj8l$MsB8$>t4URoUXDMpFUoDX}{<^hxofHw{$d&UW$xK z#JI|vR+gPy@=Y>`H4U5yGFy1$QkZiUWzDq1P0F|+#AMH+G($eXsHN<-gNX=w_l?|2 zH&RTSnXg#KfapoFKSY{pVS%sZvsUu8{VY%j;N>l&Hm4l6S_!)}dLw9jd=M*u^nA^Z zre1c;Pu3KS2SFeK%Yg^st{;VmK9B8*PG#jOBhY?$jkS%@G72cJGDChTyf5H#pXMS*X%Ddg%>72d5rWVYbba8dSY6*NB1hnKvi^A)WLM zP)VUUx9~I1jW6*xX}+s#;@TS)Vr4E)Zs-thBDHd#Bkc5uE$i3@m$WUH4Wuh^e)Ta> z$aQ0UcqKzmsMgiSlkW9ERLo7*RIjC1nS7j;saOu9LkhA5|4<22UrU+ja*$t@S@S~U z(M^+*o}2N=+@PBG&>jSm1Y;o1)n=e#Vj90G?*z$5epBDhyXxaa^AaIy-i1clFffdLPnF{vDh(AoDS+VVy;Ycp;D~=nBjTEfL)RxIfJb@R zjQc^yPzO;ai%5k=&Fz^@%oV#V0_X;KDY#G`G7Vn}({L(lW`oJ#XK*Vj6@58N!seW_ za10<8`U*#eQ=nb2yZj;fi6~$vXA&Bj7;XF|vjS*c4*^~ZNazhD*SXoawT_1xzGTGM zy@~$YM5>JAu84yqKB9|_yoF{Bn&A+9q#!p`d>6xw67%h4As$Nf#VUCv;sp4HRyFq_ zGg}D^#(yd_PE^iQ+{N{&+)NU5n36h-socVdHzJl)je}BLo|$;*gK|=Z?hSw=dD~qsel%k zzQ*48d|YqqBclcHpyU`*a)UW|2%ml2N-%b`}Ucv2_f~I%o=Lz)c zREMpU*MC=Dv36OZQDDLQ z({VVB&aYmg{jv3nSeX~K8iZUh+gcKm@W;w5$wX|UM1G}v-OT8HJ;{g zMQTzusX46_W6w1u89VXP@DtZ+2n$bUSxCydq+)I)SLQCGx|5LP!-r)5laRWKiF?=T zVDt8bV&TM41%KH#ea$18R9ooZyL&q$a6gDT_aHp`;yBN{{NArp(ohM{2fw!2(K0!q zaw4}Ws~bAfzpo58y|b*1H-S!2ZQj9>At)xqTETA(hI`=RLIWlYuU*|Cgx7cT6UqaG z0R2B;FQ`dFd@S*|pJV6S^Ku{bw9z=d)sSw+_n?TMQT1#(47qFODzJFGzQ%~~iD;x| z4v&n`WF!1=grFBSjfFbKg~Aq%F1%NDw({{Xztyus^KaQV=ogna7l{``a!*NL z4+C;G`={QDTzqv6U6D4zo0;Oz)AsrNn=nij` z!_1u#Q^0nQ;CJIiw8!??k#`&r)xmbXAss}Cs`nX*k#r#$$r156LOH`y$M>2=SkNZc zkz)BdW4VzX$zu?}_pzfxVN|6Ifm-ZWj_fJ#`2P$AKqOw#xq;}66W9{|+f4KyP(anz z*u==%z|q85#oW=(?Qaeq|B2C6l7g&!p8#rRh(fKJBI@)7V2k`F3Zg}!nHV7OAw;$^Z;*Xj0bar-P7LP1)cz`sy`G5`b^ZdST}ZP7`$1` zE*EC~k@RH$rbY;Y4|?du zG8mP0!t@8I7RNLbx8$TaF>T3GR9-p=UM5Z&2 z5OHED-0Gva$u2Q^4;fc@yoEm;Id_{Gnnd>viIvxH>(7~w6*teVe0Aht89@(+r8i(o z%e3mwy>D!EJlyUp&rByBi3>|gOnUtnWiZwJGU{|-6EOpP{*Ao)|MgZT&gOQ;3jb^0 z&i!Dw#0LQZ!3$w41`*UbkNm#N^1gqcIYIL7pZ^vv<-Ii@ANiK}1yO4-Hp?$APLP1Q zjz*E6oRMB~Bw9Gb*vn&!w8c*^d z#VR9B8;u0;WP5m&iB55b4GxwKg1`V|05kx^o{65XBx#r$SRB1S=on1F!fQdlkRN4RMBMUp47&x2!Z%{Q+%MMu(b@&ssv2lDk zLzBKKvey)$&G184unRIoj(nAyP*cERT((`ZumQCLg_a<4 zq(PaHZ$0f+a`i*^3{tcdkz56Fc>I*U#Ai;%08N0ySF=z~1gvCgjJJBbCr>X=qZB{s zjx)(8ifi7ZL_%e(er`dE%2Nr5(L+xvl2+*zE#>+x7)o<~D64*!_(JO(v8 z8^+2sidMdkAWVoROqh}3m~wK;8|_jEx8AE0ESj$}OtYREj?Emm_l=EZn-6jrP3Hr> zO{8?St%X~eG2znjoS@=6XEZpCrCPRup9W+>af0tKu4tlj`Cwh3NtRSqoPtfwaBPis zyTM7+ts17Y-gq-D9uiY(3VRGZ4Ww*utr9fuTFqLX8T*eLK?Gy7!G!$PD>e}hZAME$ ze&+!kdH!o*$;?m+&yi_xHX57CyCteggwd3`EAF+ITS6aH$RPz zL@cCg4?3GD8l$mY_Vp)$YJ@2r+EOt9q2ibzDyH#jE+>R!WBfQ0>s)bu05d){;0EEJ z>u0G}u(LkUE_w*a9{Bd(XX@X7mVeblsh#|dF!0$`OEMscBuE=BDq;dXP*lCFWWtQ> z&nf|@=76c&cgzsLi~`=V>Rr>+v615RQiNaG+(TJX^gcc3^dkOn-_70&hRowL0<=MT z`q=FH_3HIB`Rnuj5%MS7TTkuGzU{JPa8qJlOfb2Pl_l2?C7ySE02;;uwdJzHq>8)# zJ<*aje>i6Oex+^D-uj}d@UP-%?DZJr;9O@Llrz8swIKV5nX%#QHF1xEtKEn~L;i1D zel^BghhV%J5HX~?@S?J6+q7X-r-^M>?gQ6z^QDoVI+Ts90OI$JfMLjXF;AkI-b-b` z8jOK+c`#$SWs0YBf6|wz^gJ#(&@?uVQ3#7whP26CLh|dT85-5BzKHb@H7?!No(6HH84#K4<3s z;#b7|;p9Gujl)(;%k{^CXo@-yDYwv3QTA0Ad85r@vn%y*S0rEFX7D;RIGC}zb^zbG zK85SBOJ4=$KFzq$o4R5%cq(5eP5}AYV=?4{iuWR`Atp}|w)lmLr+5Wa^(GhQh07!hb;PRWNd=-PEUG(D6N-{bRZRRNys`UBYXJBx@uHHcZjYCWZ*x45Kp%)cub_E2s*KNIcku zv=TiB@NRo2lD1a5AgX`T5uJIW3y9i{vV{jo2uW3=xcz%T%HqT^+JnRq`qNFueQ?!l zr=)MegoXV`?!xf|R0NZbP>hxpdc-p)zQ`%%6hdK;a&z#G;(?cR>hHoV<_hU&7Nf)& z9kODbnu5pN)vJ-zdm)>F>G$pJHTHX{Og34^OFHb;GH^tc1omd93EH=I|}WG@(iqcDi()MNg@cG-eS(0jPI;Hrl57xcO|y7~Fe!jBC8TB}#OYNA1_ zSs}W%W}%24+!X}WKry;26eQTeB?syBuka6`+RTPUF_GJtHUe!=el$OyIi{Y*_o`INo+da z{zpGMh1lg?dChiMExif@tP&RwkH?@$s!z?oeo22Kr{yk|vj?|}&_(Ix258k6GfFS! zMv!||HgYc7toaW9xw-%7aJ zY8U?PVO%s8a*K|}jrQUZOQO9l#yq(5IY40mP8U6gZNuU66g$gHk!nX=iZxi}1--x! z5Dw|mQ)+vw;c1w-mX@Z!F13bT5-kKuYN!-s!n2-txLwdMG7L|8FP}o=jkg%u4Xy$< za3El%H3Rppv-&A=CX@_Xjk;g|IeC*B8k0ldcfj&b;pNDF3SnkVCas6O&`p80x1f40 ziX3sPd?ua^i#Apvg_Y*b#{%#btS>f6ryuGe8 z@qO73b7~HJ0I4LKOM)^=_;=xa#!h*%lwoD;#}Y>&Oovg8qI@d0ntC!gwmlQgl@2_Re-xRkWosg z_%}*D%IE-;?1175Pl%hH_>lv#;NZJEdJc`mgdy_dBxNRN`L$8&CykD`eBVM_@B1OT8kX8eIyqh)vl4Nojp$>)6CcgP{6 z6g3A&8`pKts&6{?qF1kitRE|)eVcEE7}BS^L9=bsrqhfWm+pgSZN!Cd8Y~G9H<^lR=$pe<$i-q4zs*8VB<>Xw zXW57MO644TPzal5=aFWmvFmRqMsu{o9Uq?y0C*u*XYvZ?01krz2G1nt^BgzFP*3Hi zn35^*Pvx8f?%7e6j{Z|CWceEvSsuQy!>Xb+I$Vc4e~y5+iHS2HxOWdquX!_<5m4D~Qu z!=1^+Vhu34r0!4P{mcTf%rT2_%NIjs71elxg`C=(n4;=aDe=_XVaP6*Vwd6q2B)Pu z)Jkr>GMau>p@TC5zHQ;8Tif_TR7$*@mjfz@R3dP49?^-Me;;1*JmKlO0=00Djs9@x$hnPSysn+qdEm})jfJ@*22bznZ!_<8`g9bG>YeMWe(~D=!@nf600}wopcYjM_AdT@LaPC2-)fi}LB+ zjX>P0+F*jaLVetlunl4j=on@zc9RZvcU0S-x^mRckgo9tvBnhxD^naIm7>t*zE7uQGbEWlGOkFoy_?S48%cYU_8B~)-y;m?CiXlF$}$KtWX zj>b3u;VYUZIs!X21HFD)#TvfPSIQOghN;X_b!)Lmqui)d)>VO3ITG<>ZCEWm;&bYQ zmCZeECSkDCqu`rf*qm9K!@U`>YDz@~w$&(LVLzhO4V@i&inji|n z?O0mIU|MC_wYbczl{iXWuElJu^`q-1r+`9gg-WF62SR3-7#3FRVR-y8fwyo$qB#$= zu;I)w67_YI<768A{=N7E6cAWj$fSp%+q9^V#0 zF)2~1HSy+6EeBI>q`j>OZ>A3h=wpb4oJTuL8?;>!imFVkE~DdC{XEYFd_C)!lPkBXk9x zxdQ#pbd2OlwPG+lTh(bnqQbh%Ae#o| z$A;K%&S|1;g0I-hRy0#JG3fQKtLnGyU^9xQ8N|0H@#C=3^Tm>vwXCtZqYAmqSt|Ry z9j~*_IXB|J*(zev!W0xC7QkFta?suB1V2gJf>F7gK&&yb_l3|LMB97Ou+>uYA=<=T zYg8koEOVzd8sW=l8fwj4GxC)Ui9f64nu%3dW~{Z=4LD~vp}tTDJ&whi71+DDVSte4xML$M|9Dmw$d}6Ux-YzGf}D&siHpyod>Kk_ zYNiytk`(K|Xmdm1%pGe6mon5}5uO4&{&2WNwW9f~8#buXH$EkQOB7I)m?E2yqO8j( ztNUGx{fj}IXW<}LrL@Zt&$Z^g9FQz{2U;1XJ**v8?-RvG%j}j>FZ+VN zzfr^M$7Ou}__EZ7%#)>)tL#iB$%=Ap|BM`M-3d_uVBd~n*&1-JL0q~-{i)uG3kk?s58Mzj3VG_y_$S8moGQ`i-VwJ3oW4fJt@P`27U_SMWi*=v15~2JvX$~h8SgWm!0mz$i4ySKPxNU*I$!-qd zCVoZEDRYe9{_#ZS*By$B;58aU5J%PzopUls!Dv-ILE@O>3V9dD!Pg4B^ z{pYi5#~!J57dQ{I0nTRsO|AVOL%;ufbN*ld{57q@J4RLxc!wUe)mUUPr)KyY9lE^? zA=U(3Nt6WLL{%-CwUE+OY2}a{V_SiS<4cGThXmSz_(b0wkI!Ay(Z%}#S`PrFFU(%y zLo97pEoimq`*CgvTd6oEQB--r%#p`WhY3ned4ECQ=db@9 z!F0Vn=C8o$!2`iA)qg*k`L9dv|75MBDt1x|lo3e7sM?d!vZ{jN6U2a-<>{db(G2{G z0ls8vh^u6z6t6O*?z98?tYBFDQ2-0x(3UwqJj(fWbMo=E2h1z5F3#@{HJN1fr z=Q9ievShFc(FN3#^`jd+ zCmVfpSMayLF~atu{G^xprQ_CPsN95+PiaAMYJ43Ke?YOH!CM_!)`QBgfM?|P@`XH~p-=c@o zl!S7X=%=zdoeuZdolN;Z|2#wY&^UtZMZ*Am#~H?(Rhi1OUyE%kInjVZBGPn$gkTUY z2iYvvCy7xujdODf%ZKWxBFNy%+GNG7uw+QDC9gr*#6{4RMd z$dh_R@_41DHdL%S)DS7`By-72HGG()bA#Xuty`@*)oO`q(@)L+egyz$Oz#Tc?6ZOq{_#rwqq1m_l4lJnio=eHSs9bAM`OXTzbchKiCvHseY$DM` zN`oM+C1uobbVP#@M;prIwv;95rj00h4E>6P?#@7!a25dUl?$k(%cN$GE9GEtx6r@W zXZd#Ns!DUkw|*OBT_+H0$e+Mde(~!uKLtPr%Meb+rWpm(FawmKgZ9d(A+iq=DfLYZzjE^&PQgkFO-WE!iIT-Me^tCdrvOPpN5u@E$ip-JPXQB-8^SleHs_meo?jk&*;oGs&N1!HOzC-ra zE^~gQM+{vmB0&UI6sAenm{`mU%LMA?Ri#(!NgaGjx?U}IE1}@g|H*0^i-`gVf@c!# z)OEstrgOL>EP}fFv&JtTKt67+kQ_TqeG;7US>+UTkqZ0pud5Yd)|$$o49xxc7qZ~Z zv&)qPkba5-q#*rIw>y6$3yQj$IC?mnTiBZY2kyVB<4P!ksJybVevF-HiU5BY843OU z9p+FXe=+jsZG-QX!G)kUvP_E;0$59Q?=bAPF2n>y2dgHDAQ)CJ-l`JPMGdrtT z87cgLl)J}U5W-4dYD<|wF#xE`oml1 z=Gw(;(C(JtQZF!-s=T2-v7G}*c{H=^vBH#T(scp3$DywCo|s0d3XFl0`PIj0ChkG- zF(=sF^?vElY8X%0)fKJbIDnES(29m;(y*W`c~owGQf}XdTRtc*`d(#MF=KGIZ`?e@ zv&F+`>-k(3y>)4d4mfU@FBN-FZT^!^DYqjq5sz^F{lY3(3ml|}^*jgzfs8R;REPFC zd9SrZ)b0Z8^GLD^`Xh6C?vfqVT|@ixUp-`gV_n@iv%lQ9#f@om+OHCzBl}TG-)+cZscWPQ}ybcPP6;B49b9 zYw?!@O&b+bahvj^I`>QQ@W9$6j22q%3*z8qdAS^ja@}zhb3C2PhIV~E5P#bm{L+sBxFAvsu?wBM%ae(cN+rIhM~*iq)k)}v5bJa}ZWZ}Z zlEol}0K~e)Hp1s+_;?|aCNf4sj=``bXrL&4KS!avBB+ZxQTc(;lGEa1XXFYwYDQ{+ z0BS(#`55$oMkwV-R{K+2WI8;;T|0hm*DEU41r;fNw055u_w`@+o1GDl(AU6nCk#C4 z{|Bv?6f(^WrUemdb_A^P)UOZG<3^5L;VzeDpCK^bsKaLO5W~_m(pPQGT`5c0l|y3@sn~ zvn@YZ)e^s!krig0wWCMm7hs$F(5VXA)sr4Zi(akGxX9QHL26 z;d$uTJ&y0nQE?9YV<}#xI?XRtMVB82s05ks2b{KKGUJtqXr#W05)svlip!_Ks5$m! za;>->r+l3}X2YFi5U2@oNZ%qoBNWAwC;CJhecSy?b}J1j(M6sT>kn_SzrJXF`}PgCf0lymk8JMZ1A2{z9*5wKWy)Qs&6>$Gd;I*4}jh zdC>r7Y$#CLh~~e?Ie&MfQ!}u3G5Js0ry|u0rN7ChS@q~q1PVzpko*7>vL;ZQqYszO zKsG`yiMBAZPe@#hI~asA4wGWqyO(_ddtVF0LS9Xi^L-M0!L+%OV#8#Jl`*T_c(A-` zd&=g0I<4{bc?A`~W$RDu#ar2oT?nsE+Q%EO{1uzh#6^#R+BJ0L6rdGuqzPQy+oUy7 zk8>F;9ic9wt>*gb%plV&C_&k-t2BRs{z@0z%hu!IH_GtJMvy=`XYZc14uJ&{#SiF} zq5IXIDqVz?ztSR2K04i%=@G9>g^u1}W$gX{WGAX0EMRbJXO36X(4nbc^2(H`VO8Vv zm!zYo+-VLf02w`UuNW-CPTl&)p+hjpo5FgSS;uC_&TOpt+!{AR+W@IXI5jDCYi^wd z+z_o%*U?N#DW5@HO#`L)2E_Z`jG$AdQT!2dq*^0&>t29DcXUEups8dgmswjp6p2YS zaKfcd(<8F;j-9}1^1t^S8ITBKcZ<;Q9NzaKCIjNk&jAY1dWMP`8^jgqicDvsb5tJ# zvPh9nf z?->+bY5UrEA4$It^hRtTf$udKRtx(so$LBwH2(i#>>ax_3!*LEbfxV#ZQHhO+qP|I zrEQy)w(YF6ZChRE_U#XS`rOlZjEG+lWA9jN#+vhaS|!M3hRutU(|N=8tOuoF3~@u9 zJkG5)E2NDjy3xo~f%-&Ufh&K@MS$s7NpKF6LVg9wAS&Igu$1=CbdVo=|tpy zeBH_c#{T8-$MBOq5Zpqq*Eiq-HgZPd1egrdPfV@aj`1^6qtM!oY{4~P16ShJocOVb z-`$6!j~uo52kltT&=238_&dTMzruGD<3X5_=ly6$Ki=CtRkXFc3tSQa8RFIGx+Gc%&!)1E2FTK;M!;H3;m&zFR^?i?*WJ|g4_U4w?H}>X5 z;Zk`!zcIX>?+UKcJKg_m05Sj%1Y}DHGZ>6Pq?Es-AIxALr&O8H3Hdqv`#-4?n3}xu zLjPJbpMMuF(|`Tj`ad)u5@CBg7gLY_4yhTNFf9l2AKH)Tp}vSHa+(39?tY6`&E>ZpeQ~ViW3NXqR={%M6`p9wi{`kIohsX~{-&1OIOpH7TcRb{D zu1Tez5g2M9|18r^tqxV};8j?-T|gBK8ByVZAxCF&x$dI;_sSAK_YVG~_5ykJ1BR+d zfXYr8;x%T57)^pjg;Ya~m(lmxCZL+jd zPfcFAGd-Z%r=2<1JiT=`W)|g99AtsqAP0G(+21)rS74&34$(!jw2EzaBG5Kw<4wMD zwY@Q4_qxV|CtiSRR*>QFq;4bj2Gd$s5wk4x;e#^#xB6J>gYc}Gqk%tB7_Ulo3}vDm zNUf^8#4Et7G8|yLOR;R6tw9uS)^T&NWCc!LMg3s#(@? zwYi(7VlKB3K|%x)w>yJlqOFL$ z$Gg1i6O6UVj{_Q)gN<;7q#HY`fEb~%=>?#jF zU+qNqZq-8v8@I0p^I_FX4^>GJ9Q+g?b~;~(JMI!o&=DFqJPpuX6fWXJthYK7JhbyX z^MFM+kTkmiP41&8goLDjsEQZA*&FQg51Xr6Nz zjZP}PO0%G+@E@I>3?_n&!DVUUtDTF^ZAU2zI@rgP=2`p`GoZKlL^FyfSvX7wMI*)e zFT+J@*kqcrw^iag773GBn(^WQ8{&f08?|qv+ouz?Liao$nFZ6vlC!Lv?CYOvea@BPD^(w;{x6KEC?_U^Eov_8>8v3RkVURW9^zdpOv2+j!QlKzU6uxEN)G6iLXea= zESqx+*qq>B7jQ4Ig_XM9zRpMga&oOEr*th5-T{Pa!bA$OdWwq!R5NM(;KP zH$J#LLe;Ivd2a1J(j^ZNMKfVl%W-Moid4b_Glargjc z3o`e<&? zpO(KS7b*yAX>2RzVT>_}?ExwJLC@|aEf$8y2Bjora<%spg!|w?Y8)(L@`At)JH?$1 zk5R?aOh>9F+Mwpn?}42JLY9k*uW5bZ2XN=ge4sI5jU&Cb|uRrymGc9P^^zRGd1g zAo7XyVXPXY-!+waLUSy-h1M!(WHnvqYvKg0I`}DY}pnFzQz4hyX`8K4fP7wCF$Z9J#j^k?qdzDzJfcJ92^pPt}#Cv zYXluO+(pT3eMHN()e4qf9`xEv5K&j!A&^as5jk*^5D(qyRt@r2czXlcPV<@eFR{{X zUdZ*zb~(o4k28F1Z1(jR2=6n5(uCwC-E-E#gYK zT}!~3NI6_^pe>%Hkqy~v|L!qj6BHpsc+C0~xR^LVNJ4_b6lSfEEuwyy$$yrV0@@>; zR`hW=wcuTWwA*Z3KsZrKf>SImzwdlRp)8DHmv(?PkUS`-Pi+^8ndai8voscZWDW@e zrfOKRu}So**)4Ssa&W=FhYGdmQY5E__`>LwGYp11vFVLqBcNxHt&mZwzq{I6)=&uD z1|^eHdz3B7G9=r`Bi9_axfJD&_?@u>SgV{fmK1d1n*9rkJUrFSjhMogh4(2xVq`m~ zkw>4XB!S;-k7g%arjU9S{co(@=!^h)4h?gAu^+CgRJ)FRfkMK;l$aZKTdhCAs+$|S z=j<8?ER@h1r$*etnw#evoo9>g0%?a-8^y#-%`e*fOEioRov1atkR17xJed#*m8YS@ z5yvutKu{DjIDV3D1B}LK7dPNe%;74msCwLFq~alyPubI0=)aT5wlp(tg@7e!fMMr4 z1Ny#zxdsdI?1j4%s-G@w04H_yrueFzP>`gl%omq*1B=l1$df9W6tE*g@Rn>F>|OOl z7%LT2wV=c-gsU6nV56V%!FxA)DN2>W8#C}<@pGX1#z;ldNxqyb(+}auC-gGt#1ISu zw#p{Z#snJEC=waYnw)#YUD6^t#icr5}Ni*x)r0QD}Hv`nw zju&@J|V`f1$CuT7U*TZ|7`usnN6lHbM&Hwcbl3y-3L&xDRk6Q<2GOT zTu|VJmm4T}EdmiGQ%cD)Whbn?kk_3oS7*jZhcL{%)I~lc6;U+)ydl;H#lH|hRvUy^ zc=<~EYd$s|JyBMtqCz`EeBz&X4~o?UCUP>`ow8FmfFpBhlYIR~3f6>SWa%n7l&s@3 zWoW%hg1eQ(MiEw}S)QRgk(HT$;oR>^{IxtI@bJ<%Oli$^&H)u+- zcXCmxv|xoe1FdqQf(8hVHD=GHLQf<02XAt00@b6w0{8+oDn z@rZPL$9iHCsLOP?g8NGYw0g^pukXX#C@0=jpIH+h41rH7QHG^He4ftcDPjs!${-ee zNyYHgV?J5MLhlUJCGH!EU=<29GdNR<+73XjwVTzO#Mz~Q<`%?Q7Hr+b8#dZTHO zp4L4aj~VR+N%D+#$P@Yd-lHW74-n)D!p6WeWdYjAWJ2;y_(%ojl_BK{t&M0xL0=-W z{Y2iBvTK2T3=Hqp*k9aJX28DxAP+oJ=sCX_=09V;vP%u_I-?+qt5U=RW?-UbqSMhc zWsomPSx+!xolWi{DwON}RWkbvJs79YU|Js8{-}%t2hm@uK1&zgX+@4Gq=XstDjCa!1XUTks;DsTMhJe~ppjQVj86PxN$$3)^vAeMS0RWEE!L=kH2TsbX$86}qWwk&-!q9B z_X!MPIx|4LfyDAtYpAMrP3GEUR~P}&tCSxIZa(hJ9w%E=PNUfJEEs~^=) z&@I~>7_&5&N(Apjr?7QTTN1OhyZ)HHKv&I}*KhJ>f)hwqU^K)d^K&1-P8?zRT^Lao zDu_BCQG7q&oZfc350dVThL-#^Kt)-uyhFr1MRa{a_h=N52r)Pl`NBM|> z7#rBD=n-J=Jyl@1vZ(8qFPp+E1sRskW+C_rCO=?`X~w&OU>^!1ZK`C8NxZYaZ>fxV z#(kb#ERW8xo_Hucj&WbH^U)L-UM7LQ1bh?TeXfib1TVFf)aQ5qZT7p^Xd83znY%Pw z*9K=e0|I`ywq#2;>ankDTFhz{v4$zB^z5C)%flqYhZD4E>;aM=XQ~`ZSlUQ4#cRrL z>m9WR#9|38KISbXFXkYTFIx!yUi%z6P$1|s$gby9cV%S;O~!?4jp2ojcNn`Y4V=oI)728v2zYzJ0%|m9zvA>%Z)UC0%FBhL$9g~`*JG*1$yE#w>r6U_h9lR9F zx}$aE5cY@;P&yLxM`F!E$h7jun~7cQ6_M-*{qkttQ2qP^?`Ob&f95L+=`R@i?E{M* zZ;dJHtoO;>j_VaEGJ|$1Q5^wcECVIyA>h5bygmQ!vZwA4Of7)!z!>!?HljvHPDNJ` zGPhBOnaw7b&AQ?@Wrr_pl>frz-+3TEz!YeI-I4J&f-?Xo>2{7;mOC~ET7m+6TlAKm zvuZy}b=R43r3muyZL7_3Z33)UJZI>H|l=-aCjIFxiouk?23jho+8_w(BHJI zcaovk(xo4^rX4%|7UsKp6q&m_%09fL`QnP7fdSD=Wn~S;3Zc)3#l~g97`jikOAMk9 z=CARTB0pd#tW2qtMR$pV+Hb zGvTYo+ET%XDFLe%)ZMI)g4ktMsF+4Ebr1ff0zY9LW0hS-p(!Hri*dTX*0tl>gQsb| zd-mtI1V6UINq=_Iqi$PWzPYU$D@!$IvPa4u3ukOr1UbOZzNvq@XXuW8)ci-0AW(@u z`N#YZv*6NaxH5~{W{P+J0qQzqf5?A8gPW*UK1;Oi(b|z}CNHc_(x!&VIc~V+ss#w1 zHZ_uab29D^p|xr-|6DWPCoQsGajI%6LKPHC4>20sDTkz+Axr!z%StZvRo3Tl>&c?ut}G@! zg4pS4$0)9ssUEya=?tq%5q-E;)s0C4C;)KWyLoLGR~%>^xQwE=&1o|nd7wMWR%?B$ zy}1Kwi6EE6tDO<9jyZy2U;Rn6zp+GD8gNqrik)gXGDXMR;kD_`bS!a5 z>UECy3NMIAs-F!9wHJXwrIduF@>qWR^kXu^gwP4q1}d?H2@;kd5zI5>($?R%7%MlH zw327m5Ws8{=}K8cQ0{Un3jtCqGy#IU%$9B8AuaN-sDT{;iJFpBn zKIeK4PSNDVri#OSw}?~%z3mcs#4EGyHs(u6TPex|D=Qd$Mg`qmBXrq_MQWz1tj36- z;~b)=PU_L#w#UwhdwS%;by%kicbzrq`PzcCP~EF94N|ZVGp3%W%(om7+Pw~qA1nNF zOPY!*(NT5gO2z&6du^Ir^Oyq&{1A56D~KyG-Oe~Y>cd5CYi*G|m-6a#K2S-lM%N|3 z5v)LZ?K5d#vx-S*S$mW!lNO|mWy4ZEulU<;WE!hhwX|$WX*xgkVel|F z-^m-5hpi}YUXLr8pV>Qc$TqCn!hjvktp}QQ=Fg{#N+xkezPvozG4?lC4e^>{Y7-Q> zPH9)>b8UyC62+@>c&_d_pOQ@f_zr$sHvZRxkn(4Yf@xG7svSlP$b#0EuAy3O(Ygl9 zm$P+@<`2qrK&^S@#qFZd&WVi??s{7DFBdljZ$xFSQ|(qoxQh21V#EB*etqFSKh z8?irip;e7I<3-qi0}z*cW(*=txbVQVw6!1f>|xj0n@1&rMipVaexY z%&kAXiWpvD!n?SL{lO?F7C5i7P_p>RM~zfHN*s(!iT31Btr-BvQjLIPt zn_sRoujeS#?k-XAaKmd{pXMI$o_tO&YolE$s3x~V^iJgO&@kZl!UX9t2 zhw`}@x+#O`LCK*FyxDsFis^g#u|JgBEuu>FQ(ZAhl)07!;ekX37BnLZKoSW9h_)*R z??VRt>cAs_jG*giv{FGgHC1<%eKa;zJRpkx-8VJl(2?%plS@s7=Rs4u(xG?JPPNnf z@zf#1A!(Ov1Us;;X30$VZkb!Yh!?C1{y$LJ*2e5J4y9`JjDc0QlQ0Kl-)f z$d0#}fK7o}Sr(1#B01I?OShQjsoiYC8DU-f0XAJm9dYO@e34OCAfpvJ&s`p2bvFf~ zWzk++3VRFw;=s(g$3a^Y2($yQg_`7`nw`2lFA(zxx3cI;tAg0YC&@m7ecHiwUqmiP zZAC#ov|(Y2YZ^LWq21u(^wyyP%er;?+|$sDzY8*EDyO2?0f6BkagIJ?5m}32w_)b3|e)0@V4}Ah$JTUF3B%_zIcD7G11| zulIA@=r*I?q~*gGgy~|Iip;Nam~J>mgt_2w%>uyYiGG7F|WJ-Qt5i_ntVwkdIBb&dvdoK5P~!vdDn;^WxOQS&!?+8dDgW-L=% z49MnI{f6Nh?I@;L#Xvpi`=5 zEHZAt?nq+$p2W%WeQALG6;(mL#EI|+)_(pu3v(S>M%&UC#`OJsjGX=NoMK0v7~niZ z<8}>a3xz4_nw4*oU_@Xl>)9orYTu+WSxmcZiGNSQzhJRji zuTkntLDMu_udd|#y`RjBIW-@&-+kYOq52meq#qd5)9NqBcNOPKH63>gaom&+rw8Fw zRvhG|bM$k&;t#y~eSYTsi!&G!x|q#v8C9mPtkMAEF1t?E(#N+6AyJG*PueG`!IblF zf>&0Bl0Jq$8|$2S#B5*6zq9tI+N(ZL_s{McFd5&$G({g88@#qX7=$Vji=95e zz5~u@(mm-tC;IjA^n#q8l4n%Hwpal7wqTsl?G;qB;a z%rl$iAi?*uD3Yow0_;oWd|--e2uj=o)fSG@m6HBZ+i8wqhJBLV7C2r6l&8cGw9rqs z9iShdWF1Z+p};APi?bAdbM*Xy8n)*#MoTmSSBj+eNCcE6<3fw4C;4oqy2&)z8kST@ zvPqjdlj7TFbU^L;P3*f$B%zJT+ks%hjAUT;@}o-L|HeX_PSy;ph!$b~O;_X;)Vow? z>|)Tiq^~(EtX*E89-grt8h-w7^GSGm&5h69sDB`V&IU_o99WlvjH)LbGRFE)op(uy z63Z{yqR7XgnUQ*EC6--uHW?A>2N^%xnbGt&(ZrrWUpd#j$Nk!-V;KwiP3L$0!qU65 z`01zaESv))1h4pVq7H3?nQqcDpMck`TNY=u$N*<6dJO`U4xlMsxc#5~@(|cw` zc$vt)k7Qz9c>$gm4p$s=vcu1@CR1#*{<6@?PgNtTOwP*qV6YQp?b zYd{<8dAvoqyN_JhL3j~I4;l1`P#k|hb<*dCg&9g+Tqw%thec|V>goUN4!K&sfI}>9(j| z@S`GX&qBu`SgjG`7~=PeFmG)3>Efr&(B6X^%INJv&`;3!##rat;cLao^J(sK{+r^+ zE|^XCMI1knuB`q);Gen0XrC-?SHJRB&P@H{xA%h|k$UCx9{5kizkfHq5nm7CJG*~} zd_44eVET_J&&!?A2Od%Pr2oXS?pf=BZLR=~JALQcL&R;o@_%xFCs@Cd>y7e1#C zN9lCRLh+aS+#?t~_J5ra;r?XW*SDX0WmQ3BtipRYZ2ge90-V3?^Q)<#`L3KNX zS`J%>$5v8uE|XCSV_H>vc231#sjj{y>n59yw=EoPrJz?>EgTSjK;<2kg1v^bUF2j{ z&VLPfdUj}Ch*lxy`-f>~uk^vUZRu{KR%+%%wG3Wl9d$uCaX|~|h^ZOJEcYp86_;;A ztiV&Lo~Wqe%A8PVfZ65UrL2#uq!HVxxm)~y%d`MTUcrJxqhc0}V>p;amU7SWE@e`!Ib`KYfZxD zFFv4%Ft6M|67o*&f|uA6%I?YvU;PnTcEhv{qlupY47TpnPpo9$HcHyX2@V`7jeZGT>i=I_-)zR0r^y$v?pCTS>!j0uMYEM+R#Je z%Cv8#$YGBRKUwb;yi5AL zacGZqE`{sS8va3M>I)e9BR9ol*Av+!dlg^$kskFoRJZhf6z%~6{cI~rkq{Xh<(u#^t30+MB6q6Xwz z3tcUq;ER>dwN0o0FO=EJVBRWtwt&Y46gK94tv?7n$xGT}ald}2?1YK;2*-J)#VOq= zihLzORLUpF{(zU?cq`a9D|}d#`#Tnt*1IyTvf%=2<0JgPOB0)oU8@JEg&v7kzuzSM z)Iw&KF-S`C=Yl@kocq;k=72SK8XJL)(#lZ8I5A=C3hY@-U%1#?XZT64sKDD)hZp@U zE(+IAk2Zbf%frC+kYT8#?feYQ$~o7z;g?0R;3ZkNl+C=P59)8&2@HNTX);_=%_Zf= ziaM}Uj`4zmQvj)_OXA^Tg%Xe~-y@6Xs1=j6bz`?G$_f;$WwT&NA;NU7+LxD+N5fzH z63>}s8~Ri9qAdWfHO7cn;(}dj*%pm_C-Xux^M)LhP^XcFx=@LdRGdqYTTVrufj2dL z4u!5|h)WTpk{C)cx2&Ll%40s3k{t7qg~dl^*_CMvB<0A_pv$eD0XF}-V&aRTc%#@)??uEk`tQ?)JqUK3`X?`s#j8+fn| zuAMQZyD#mFNg5z>Q>#Y`(~?z4Q%SIWArIOV$TcEN|-5aK>H%yy-kkKGMu$1CeF8!ig==vAPOPkP}aopZ{&LbDzX zEC-?u-UL{%2RQPP^Z0mv262g5orN z$V$l3WZLVGyE|c%%FoAi2!APuvvcmXp^7@^Y0mn?&#%Yw)s1 znzk;iZ9v?8Y^i9eo%0xtn)3)Y402Tz$7A|Iu+Np=eDu#1w$uYp|`La<6uUXlUL z8DIcHW1V2+bAry%c`4095TlQT$l0y6P_UQH&fm^b!61OyuIaD&i}9MW@9hC&pHgoL zXb^S#x|F_@o+-UT76eZMwXaHozEpFv#9GcondN!^O|*cImp1G8x(+1b_4{AF28wti z*poyhnwNlj2f`&CmnNt(*3>V4vIcS`zs)&CO^I-$HRt}V=tRr@hGqK0ltSLsw z=H!XI6(+!!b?-r@OKpre^+u+*uL4}xo7{Nh1Uz4N)~9Q8 zg4h&1J}HGq((WLmF@Zjz_ZDPrXl0_vr^)7++Z1Mp7;est+CD*SYK|>V8b>cH;9JrU z$YtavYDK=t4|!P6CMV2Ee2i|%#7bH1K@;S%l^T0 zga5G<5+VARMDJ+`x&l&;MFl*dJg@A>tmzLTBgZ^yU&XJ`tufJ?LidPAg?*Kb8rZrlNnEt-G+8VuD1p7|@pWL~NJd>8}?qz7xH zwNy4PP2A7tl}XGTRgTWhmT~i(S)z;!#4E?BSX7-$!@Rx7e zHlaQKjM-3^1N*OI`@~nJUG4Y8o?cY)d&4l3t62rVszvI%Y>DQ1Yf0&k!d8KAnCYd% zueniT*QK40+}(Who8VmPOLtx1;nm4TR6>fSEH}X9hXvi2RxR+6pS(h}k#-*S(O7u> zk9hshD8(vY;1_>r?>EfzPYlG^N&OcFg=Fs#$Say`a$Q46}(x9@{&UWi)+s%Nxkj;beiCJ{@oT zw{H)K*PyzD{YTX44IgU4yM2g;IId|g47Dt+LnADVj=Hgev#GE}=~n&-~1gU#P+D{p(btz$LU_U0zwF&QHOvWDL2 zgdLz-YC|BgM-4G`8B1>uEQ5InyoqSk=oBYyZ}fFxE+fZbe?!O4>PDK0Dagl)G;Wq( z*SBxW_uLEdRz*!3!WY*?45r$#mWm+`OTHu=Bj2Yi}< zvOMI*+CaL>v?sGyL3fXCEWNyBxK7HZz^2Ev$i|Am74%6n+5F>FFWE8N;l^cH(kN29 zFgG`!#`MEX2s@fo6Qve*3G%OL8wY{zOQD2jezGw}RVdFcL0)4BDg?l9D6f2tZ!LQH z7Vo2qzev5@M6)WxyCk!!!%SUe@OaNX>t}IY*Ks@u8CcI>X^$TjcfD-~^hJXwu~`X! zuE%-?msc<2x}xCwBhEJTXg-qe&4OttQbEHw+BLT4Ln^oT+aEX~6@B;d`~H@L)at2R!gL(DpNc5MF ze$(rS!q2bTlf}2l3nKT8a3h)^^QFi>nkD-h9d+I{_oP^XcEl%n=|%nrowmi;aQkDM zapC{->wljXmdO_JoBd}MwLk&_qWG_;h5sj8;{T5irAF<;8&w_4PaZjOngLOQu!T$( zU1P#57)recu0#Nvz^$DW5mxcs#MupNHAmN+jAULfMaz3pYo4k_ts!%LAys;Uf#nWtdigU-acjLqNzz*!PAJ?ZP-JvVjMV%^< zNvw;-&C9sv+Lz5xGskBsx#6(rh8rUA%Iwt8Oxx)e&rxeTw3%DH$053~43hb4-gBM9 z8q&pgyQU!UF&#I_wH&SdX$Sne#>4>k9Q^zt3tWr3V<8XE8LA3~=1vd%uS)XMH-1u* zs|6zDlrLp4JnqHK8QLmNx3qr;__>jlAQwpNpdwmIZLuL~2ahA&O8i8z3}!8ITbai0 z+?XYnGiAe&BY9G@_T$~wvu`=}vG^7IBGXn5$nxH^AZ~118J?;I+A4aFH8+|7Oc1tJ zz1V1Xo4jBRO1YzaMl%hnZZf^G=;z+rP*ELLQTE{iL>xD+>vXpwK zz(2e&E*DrTN~1+^96e#RAu(45p?C+VpN-Xk?^Upg3vdB_#X#n!E%}Jjf|ED75z|4Y zrILBHfU=-n-yd1(26Gy`w8I-Vw9t=#nLTM|(~K7}DR`|xlt&hG(u2bNKzJsnu`*&> zMIeB|d8O*>XHR85o*ZAuEoh%`0cKh~3A?gH?-02TOMh7dRgZ7@iR%$O(hcF@Nvs3X z7WF~3OH)U#b_=SG8u(fA;;6#wa++h!HB>rbY4D8@`tm7JthjVqxZz3kOpQp&L-CES zPuR9i_2le95P+BLOLB8?dk0kMl|heKPzB>CT-+FeENs61Akr)~F?v!Q%6hY9Qr~mq zvD$In3L55!G3HYKo(Hg9QxJ7c$(#1ZiMZk@qMU_B*dE9_dMNrkj)gB*Ph^tS%K=wU zgc+J~QS-1T=@$Kgq?h{?YZ6T=rYmXGLT7;u!2}jH(h38#lFfsF*vM2HrTD`hyA2jg zjRsc=qmDBPW$=NFJRvb~ItaCldFw$s)MS329(RtCNzQBy3fBxlMP(WYwUkOj{Q~Jz zvivrP>%Hx;Z%g`<;G$;VOc#6_DH?3n-}3C;70s6AXCN{Q`)ud%*i|fILWB*=w5TJC zP;A4np5U9x-@N_bGwJv&N=l>3m5A*7mVu^m{O+r%j?O}(O_e}Zx7{Fs7Am%C{d}$a zaHMXY@Do&4e}P5kiQ^J>^EHQdh&$n~d>dayg9_5o51LU{8rLqd0bkR@aQ?+KC(5}y zCOyq4wAX|_(CA`{-Xj7%)st~~Ok*;NHp~Y~vr16$Ql=P#0e-dO6HIPz$vsTDi5Jf~ z7a7;r@{wgVNn!g57v1F@|M6=!bz;8-zmi0?PSNP@`3LOlI#!)NZIBD9 zjAn6c@FqX%`j?-At(7lsIE=d%0qailsn$c01YKa}pLLCEE3{Q4>EmmK z8ogl1`GDUj2%ZojJfNRKON_zRPfI@y{jZl*ew-a=Za6G0Hvko!~TY)h-PQzOn?6k>q>;~S)=(w zc%W*+8f%Xg{Q=<+ci|q@ipf;=`_pGJy~nPEQCM*}AzlC1A9X+80ndI%%|oj7(grpq zU)%!f+}EAgx{qj$_+K2~IkQeG4?n0|5su5n_i&IWeJw|Q&12#bD)!@r7fw)#Y9T5Y z(0`+LVUZl-sq zEYDk19&?8WM~{{4Xz+WQb3^sOSYd;v1fz%p9H30&%U`FHn$1ZcXhOEd2V6uYZGSvB zwdgPTYL9Dp1s};dA6R+R;4k`wG^kvMHD(bnG-v)rX5mZg?7=bs#NXG6XT4)sPAuI{h(kM6t_tg{E#i4)^8ze4b;9JJONn?RrVMy4R?AEIKE z-8R3!f}ymxmnXKbIX={6A1_KdKMm4uKZ@cg8c(Lip*!_=1mH@U(HUczP3g}JSmGS0 zhyQTRte}_vbNbNh@x^a*sgss|drNBW^D=uR7S5X7`S$SD@jRL9?=##p`soB)cDVtx z0{xJXt?}bq^7eT^J@7{Fm>2S+Mm?eVUyw%?&=Z>S&Qx4<;!Y&ePwoix2w3$ zCpUkf<#t&yMPyJeXcTh?E_lNH``2JRlFVmFy8`_mzW6Vf^7d`dJ#wn}(6&tzRk`HQ zODxMtWH(+f^M*qU;v~@RkA3hnM(pF{33;NUL*FbvbDgWq7*%;*X6}@ta;B6oNxmfj zK2d)f(*xZR52xI^#PxT`zxV}E(>x`+N5;Cv^?f?MolxxvWck{zRNn(>YIW!omwu}+ z1yqF~ilXI_?uvV`{%WKCRqn1Lw)kMdRqO1}xDE%Fe%bE!_li~cIaYHCPIow#+rr{= z5!MbtcQBT6$tkQkV?@8UKz6tk^VG5OJ-o~Vif^wiRl(Hq3#DQGZ=6M;sypfcQOxc^GbkGA{8bGhe?{315&_|P zHSPDoOr_Y;B{oZo8f@|`!nMXJCpUA-u8*|W{ic)E)4PIE^Bte-AEhrc-~y<*?50lU zotLW|?w8J{w~y((Iv|~aR@kx*`@crkHCYy#)o>W3rcppgo%k{Ni2q zp@ojBWJq)@2-Qir#fPCL7cfO#kHM7Mj$sFGXOkmtwJ9~M7?~EUL9qcLSYtMt7+7PV zQ5#r8jreWj2unR*z-danN36mWlA*>fp{)m<@;O>lI1<9j1F0T{Uid_XU&~9V)mv<# z&eMSeYC3w|>3F0g0Su`haCvk~r#!V=A`%s(X-GGhK0|jXmW0 ztqR#kbzZ$3@*}5@fgv{jb4R865yuQOtS6uSlE)kgBeQ7DtEdT$jzB61KykH^aS?7x z$(?iMh|QC&K-lwMf#l*l)yxGFAI=jCrsI=145fDSaYbbM&a^yeOwmu_;@N@Ax!4H% z7`oOfT|Po{vF?>*3po1PZF+=NXXR+g_jo~-M7k!@HZ1b{SXpWeV(`$=<1pVh8EXbd zz-<;;#jet>j11udMc8OG$$gkAo@eSkrjpP&WaZw!*78&#u*YDAj;dvm#oL(lFtN%CVnYMee33g@nq37e+(^ zgD^KvzG7(LBZTjTt$rE|l{r6Z;0_T?2Pk6kmnpJ-OpS{<`iJMXJ0>M_YNB`iRELJk zU#B$XiY3`DJAu{><}&&ectJ_u4D^&G2Txm=!2pZp3t zXkc@C&tTcu5&%5B|5LxGL)rMg$4z=aKjPzDkocAd*VybX=i5amaRx!`7RD|S&yg34 zHeRcR-N4;%IhV6MLO`yO|A4u=L%m4krOfD$LE7fSE|wD*@`z28?`JmzW#56N#g?^6 zHU!gQf(;mbu$}gRO#|z%+E-3rIShsPl0?w2 zjDqd{Vu6hSRI>Yy6zO8zjb^7UkgSh5gyOjE#XKAdyc=A}QejA_&4n~PEWSndL{8U7 zlf)md@Nn-@@M|0k7h>CEnk~u=h=!oxilz#}zzwzYvy!!;F&{-M3*qqw87m6j1@0yB zcr7oPNf&si>eM?7dt=AX^Z^_20+?H&5X& z=5m9$ZOX%Lmx!x#Nz62Ze{qe^6(HRe87vQFsFP9VulIDT&|$lcG#^8s#JqKQin@oVp*! zR?&v6M6HQ}_~@qIAURCq$;>mi$%#h!iC=s7_) zeCYV=ZZ&D;53sOyoBni|uBEW}Y*s~{jD@IIfpmVtm&Cb1Jwy2@*?9*VG!AS|iVv2N zYM}55*K1X+H?XA!7(hg!9^}@fY&^4CMR$;8g|0Ze)ePC;N~F7$JtqT6>=V}hBj6&mqKyT#y~uVp^{*kPIf7jo*xIIsZyQPsY0Oo#MiKQ=y|^d zM%^5QCql9-x4ge%osS<^9jJ~`e?*E4d8D{IE4E6D>3?l?&DQnTdJbA>49Opf+%va$ zQH@A1I;t~X=L&7|h9cJf)>_FGp&J_5SK zC=mS7!LgVc6H3iNhEHi()fqe(sZ#CMjuI;&zQGz3zc(ndHuV@(O5?2S8#y=HH7w-| z&7?zU`j^&VO#)&l1`%z7LYwOka?FZD50w6Z$J3Wk8g_JfASuTrSxMw5QNP44oJ|DS zfYc{bkYP#Ci(;wTZ2>f&R{v=6{X_1dGW%!y5*6Lg|DfzG!|K|aY+-^0cXxMpcXxM! zySqcMjk|k*jYDt??(XjHF2M;O-Tn0K=e+klr|}`G>ZV*RdyrATG&}u_uP9i@5baXj3T5qF%`s=tBr(j*JgG z0tzu3c!Y7={$x(y?xV!kp1g6Yl}HV^p4yrB>fX8QU(Q!^P0(l}ywhw!Q~X5uQ= zpDpnE47jA<(J8Mvf!7`+@4+u0`MGVS{IV$NdTBR zj_(c$38eCHlPv1d!Rpb5k`RJA@ThtK=-HXP%J0~uBh}MRh=*ib`v#SEM$jqZ_YL&) zJ4aDa27SM8nhaQsz^eC__9o;e?$O`uk#laD_}k?ZeG^9z2`S#~Wwu=3q5fe0wH9aK zM?7ZzDX@_GDX{Q2c2WOj%p?7&so-p4=J=@%^?%B8|EP+WqCCY2CXDunz%H%P^6W7V z5G*BYysPJgET)1N{^}~fmU5xM%4_#j-4f3gUN0!qt}jmYHOy-(i*xIBGtSG)&lj{N z6h&BUG_r;G%^Yi9eCYTL*QwH0)__1<1KYr@k|T^pMWU|0>Eb)oGwn(eNwTtP_F{Fi z13nz8%ofnsE}4syB@%bFA;l@4yz-5{BgGQMnO7KXS-;n@)p^GVypVy#5gL09#<170n2*H@i!zkiX;m>a(6PW%+{e1QF1r5`aX z7e^yk6HBxI*~))No{Ik4&ITzFsaUG8Kv}F(Hz++^wo0sOxL^i~S~$`*qHmu(Is`fg zn@U5sO8?xLcB7){{nVmS+-mCN98|#$m)0QL)P|b~Aj2mQ|Db#dv4-TR29aS4)i!r0~{{-8q2o_kInB6YGoza3wf>Vf|&3(5$qr>9Jqi7U>ba3j~j(RMHjq%+g}I3~$Fa1C_U#A*%9T8o zguxbHcO;a0a4M?;VR27__{(+4@?6^Y;jBx8;B&kd>V6HmtsRL+|`!tgVj2sz3 z5Qo^*qSEh|5jQU-*Njm~4_7VIUy5Vlr1=B@)(S)A)`v0Lm-~L~xA+yu&(!m9(w9;F zG-P~uo&mhW;iiE>!DN%`(ldSoCZB1{(k%niEyN;sd#BSJhvR}ROZJd+=intNMPIPz zYWugfS`s~p`($Tf5cZ_#tsT|sx?^#%1tw~W5?7tIoS(6HLbjbgo!1&rAh4P=8d~M7 z^rmZJH$>XQv_AURzGa)1j$qEq>BOt|Q1|R{DbSV<(e&)G!f22ZnfuKTc4@?@HX^3n)n(D&1 zvYd;8oMf2?*NqPh4AL@0G;}TBK<_>pJ1KwBA!RFQUr%A_Q)_~BoGdeh1NWNRUNoES zw?GQhanD+~%v@X8U|Gxo{W81h3?kt>88hoRb?HXRFPl*f zJAD(bE|tKy4~#S3%7X(^qL)8Bg2)eG5RFRUZh|V;*{p4sXa@bG62S6sHFRyyci*Sv zaF+Z`43Fg&TD3(1)uuAs)*}Am=)Al=z_GOflWp}63T@wp(S;3Dtqa!!b74gb9GKT@ z^a15nW`lea^z8zZfh zY4}ZEZh_v3-B>~ObT;LkKiq6o+r%eh3M8z{>2?J5 z`weMJ;?s7cRl8pEuSpk0`iwb~=ed3E0C$gR{xJRB^YV0Y>^~y9K5jMM7+(p9-EmhI z^?Oda+x0@-6>Z}E8;)I#5Gc#1Z)tHH@`;Z##7T}@<2GNVCXKflPW!E|1pt=l_fR!U z7R)SBl`ZAGynDt1!~%;O0gQ~E?uqAXam35%KrIgCxQyPlQUKx-b4-+}ptj)EBiv>W z`K|mo!=65N3U+%KEgBed)E=7CbQ@u}eQ%@^TR})?SZ)zVcJUEkgx|b2q7Y@B+{~RX zbeJRj1&|hi%N3NxC3CEKgXrT#bW7^wWd|f#7GLm1(2J8+>wY8>Q#F3 zvvugB-Pg%N=PUVs5VfgqCXp+wG;+=@LrP>{aYtff+f;DP)_lBLm6LuzUj_SInW9)O zG!mm+~_LPqpa-;Sa65&jMlbcvOsKIDk`Lkl^WNLL)@Sq2!z5F zp`C#{pyXweTTmx!j^mXtdqof66j%&gSIQT|#{MPpfDNYaG0hn4rE_7lzHmodF=bHW zaNRXa5)6T(Ig8j57({x=9FZkfUr)VGlAMFCOfn*#`TCP37 zmg)k&R?H_b#Q%g7|Np_^KbZ}ssOo;cWoUnjAuv}%HPFN%gsYZQ$3VX!ix{7hlZg{r zdk(CaS$+lR*YQT(*2{quA`V)7ggyos@zqx$ms)x*XShuI-sN0nbHCmkwLOARFi&ki ziSW*3?k%La=2S6xHtx|uZ$N2a`q7Ih+MW>klK3J!v%s)oV2S$YIJihUbzSngEQOZP z1FnHm}bI~n=3BPL(R>j6yjgf`^y1R_(+F#J&7&g5-=KX_yU5$6 zU9W}{wN_d+lKP`~XIN2SI|EzCAA@k23u4?ptdU)opLjThnFvJ&&5kif$9Yxtjyd^J zEI+05HLzb0i|y*hGbA>Jn1hv*vCfr_x$ZSBWj_7i$6Ye8 z7{g;b_M3dIz1o7?`gPbhsp_X%6%Kojq;=!tpBGQRh#T&YXXNlHOtzgG$3;i<$#b&? z#;fiJ<~--njc60gAei~LO|0;IW=c3KsXnDKd!gWkzgh|;Og?5j6TKq5&k)6#?|+w6 zy&pRzP;EjO;dL zQ5MVAskPs3`RiC)e@#?iZF5Uz?^m=3WsX`uxwjoWD`Va4!OW1P8U$mQxsXixWSn`* z(f14%zx+Gwecfk*Y5VI|ipPs4H2IZQu(~YQEyEq&6+tKuL1EqWLVnRFH4@0Kf2l-* zOM?|#eoExJey+dKGW&1Ixqkr7|6|PRE&+M+Z;V-w`m?iv{rk4=kP;G*ftCGrV(;1g z?XG?wz{>f36n98-3CM5hIQ;Q!q+ptk{#MG)<+|nyG`OT-s9>;Q;7lwvU!*A@HU8zl zud22}z(n!A1b;2)f?Mq$-^oTUh}vlNJtC5V|gXaK$ibpNak6xdg?56OfVpeN76Q|qJ294 zO@^0)=E_6zi*F^&{0!OrSOf!9j@haK^(%NL62gZEpek%d3wf27J4#?UcmWaq!nvWL z5nC|e;L4iX83ZBpS|dUWM%F*?qJ%P!QLqbS)e>ow-@==B$$e*5%)yu|_dadwt4$r- zQ{WBjka8f@M9K>8MYO@OQ=M@ZH6POE<+?|VOGK?rC``V(qs@^-h&4?xJ1PZXpYGYD zAd1%|_zAMb7}J?t)L5RlnCH96))*E7g;S*x>Abh(SQg)uMnBnk!GZs2iwsph95h8& zKFbcWV!>&F^9$W?5{jyeP)*rJu1{{alp)9-RF5*Cb(O`&nZ6hd`-E^TT`^ zP=_3e%fDHH*EZC!%0Gal1`$zVxVvZlCZ}MK*3+D+(DnpDB)6|oD#6C;bjYp#)RPS% zFw(0*FA*zMv#2eV^XiDmnCD47WtwU*L;Vi*uJ;W%T_-ibGpW4-0^hrx7i}HBEnC!5 z7cyh|yZ}eJ>Sa9OZg5$=(;Mi^C+kN77tk~H!M2X_E9=%4tBnjo`uk3A<6#BoDn zh(5N`qha%}5x|pnc;tox0m=VlJN=FFuAGs#mz;x<>HlLsuIZ(Rr-}a|0D4|0Hy4u3 zhC3Uw%%&Y%CvQ84QA3R+(!7BL7b2yyfzTFb*2~m3IY|=1VST0^=c!hBW6f{UxCl4~ z6+iP+uUc%|Z2m1E@Mll(^l{?cIRs)hF5vt;i?i$Qm3Q-s{~=G30Ae~M0LHc%h&l-u zx+gt{1y^!0|8(Q1Cg{YMIg2`bnV8z(Mrrm+1ipwjH*{Z9raN*S*ki(S`BWogsEZ7Z z|H}&D^d`aU%?uPb63SkM1p>oJxBA(Sz16zK}qtkF_8TOoPVmsjg+S)3Vscv<#9_MC8+Tq+Z+2nRGVsTB%8Y^a&*CN(z z8L`UzWJGvS9TlEeRcMppc=thdv>8YCOi)WPtsmyKAn5s`4eWb;3Xt=|_si5_k!C^e zp(LZz7Q3q;?mIl33JU!)!%l%!1G{V%@=+?j{Pv|{5cYILmoMl>dWnhg4W2OTzMdK& zRfhh0EMYmgDOo4Mxj(2E@&~4=)>dbfBRI~BPJ&siv~nRpYfCaDqQ)5qb7h7>n*n~u zDw7+%G}@{2VY(1BavC~vXKN19fFQD;MlC@2x7li*hgDV8B zReAl?*{OA+6>l zG0$budBeRM$JTJyq8jA%NV{aML}AoOH0UiY1=j&wGKhO{Ubw4lHpB8Neair(?V(3LqzsVe)Um|c zbK4R<*1)_%dh^^Jg#nff%{Tn8rW4FZYf)18N4)`z2naxHnvHX0+4J3 zBKv`DVs)S0P)N*QAUsb);}stt$m|`fa5o$4da`a7sO3Innr$YX8Z!g^9Qv?RgdHr5 zV@~M|R0)%dWu8gIQPj6Sg=sv~cD*=NH?iteQd8LF(a(#xl5<~g2nO!HHD$X-(jUEl zJM{nFBBk?xG5Q6TTU8a;xb=uYY*plDZO2j1sSH`k$X z*!5bK&5sY!k}h`pU#c}cbxT&IA;mCRVvW>AW%M z1j*iTx|HYE96Gp*s98TD7)PrB+r$Rw+Y`TTWW@|`Ij)8V2cu3vH(^Sq;O94MPjjT| zv~j*mgik6#zMD}gO8u=?oTz=*4XsG66{|I{lI?lp z-lWWq^O#o^_F?vR!y{bY<%oXRrPTI2spT9Gll+M`FS9wqTC;!aiPpn|;mD=qmCR*i z0|*6DU<26`C~a%Y(BX~K2s&ZbQ)gw1$rYSrtk5L2y6rc2WmbghzHw#|o0L$FgR`Uu-eL0qp!wOnuvV-NC4y zRAJD+4^RmnlZTjDw>H;~wgm^mFN6sNA{2YV3R}L!?ap1_8Jo;LOtE0ua2xv~-^4`xJP~4f)}m*wKIhA?z}ZFEPklx`vF*mhCz>}x@D_3oI#;~;|ot` z58NkfQTdQFLz(7_!XAx)4Yd`mwP)LE&)KruvI4KikFUhmm3w?Th!K!L?t{${-Fbb> zUjFe~)fUJ{mL>qoG9KnS5pRhht47Z3bh!oV(`EE z-M``BAR)JTvbG>M&-^Ss7HVD|st}66IlWu)4d0vrv(4#21q!7qe}9s>?Nv)lWOkd1hG=N15RJxwWRqJI50Z1zJm zuS8pA^ND6$Re3mR6f2x5x?kOqe)s#;o76~9zHs^*HbdxQXS@Y~!br)ceN@QNjRb)Z z!iX=+Az%QuxAbzcNX-hF;*)JTXfO_co+L}LKruf`tW_iII4*07s$+?Jic`KC+nPIq zv5fmyaJD@R;vVx!JF!RlTX_1PjW2T}6Ek@un}39-IhtMuxJ&5#icsLjH8I&-gFo?& zQFWn627V^-K=R5%$EMT(2e*YXTY}k8OrnryqN#wgzksiuoF};qm*0lmL!hfJTxGKw z-of812!SW9tYC@pMDp+TY=w`P{EwS1X^);4f{X@Vk%k1(FKgf^jVw^gY$vl7#?fzK zqauI@SfSCmEP&sz62#y|NL47Z2e1;mfmUnO$J7=?C2J3&05ddKO2re%(2E7Ao4c3x zpYcxy%)bV22Si4p@}=`feA3cpZsx#(VZs2LBno*V%N+HomuRqnsv0+`H)%9{)-dg=x$@cC*{GQ4tL0d5`AO+6;zI*;sBAobn^N3SRC0IzN;~24pkX*x$|6m^`ghh z({+G)bKRZJm-zZQ(`FylX7=?^Ppl3prZ#YVy%^!_#*M@kEU!W=cD!DrFMe%+ju!s$ z$d)iO?z6$t8k@Z*bo+=!wTAdQw+&;cp01qRg=ugs#NH`DaOXVlCRVJ1jRAeT}Qz|-7WtpPQX(h%d zWidMJeL*G%2{4>~qozDH^QX$Kyv0YR4@#mBYp_tnMb1*7>WBF=^RqM!8%uOaUU5Ku z1>MDF2q}&nI^2d4R#nqZ^xEL}+{~S7Lsyx@?mf@AWlxbEA^w2V-_vN5BgdYMqK;M* z#{=1%R)AY?9Y`%n`o>L+4Xd$qh&5@Qrb1m7=c`Ip+(r$vhZ>j_vea6od_Ny;mW(lM zZGO%}rsc*+L?eK@8h;kgoLApv!>xjaOsVDIv)jlu>v_zG9u|dEKz*w@o61yIQzfT4 zd{_H}xDD>eMkbS8Qs45$_Y1QhFliUR?t0Y;K8aG2v10{K8c-$#S9cU)ZS=p(jg(w95Pg%%7f;>D zoW4fY&KXif2Q7|QjdMpR@Os$HLgPXP>hhe0Dk+a$hC5eFoB?{Q*%w;jLs_1KG|$qm zb+LOH>&!-tNxd}V3r|ctEsMUwLX?Hl-kagiOKskejZQqyo+6vBd8b0A0yAV?({c7aWXZ#by|D%Qg*|3KShtc+Q{9aqmU@j=0S?L?;yB-Taa9tl2e3 zeXs{m%CF2MVpsrh(~RnWfZ;s|fw_-VB{_U)MFw5V9PpuPhk0Ha<=n)6e%Jhi18m=Q z@i}K9k!@jb3RZKFi`mMg3~Dw)%&Wes549FTG+B#-y26q9i6gL2^=R5m z?~dN)=h4H*#j@5Ja5%6Ru;A&(9ru|JY527lkxhwDAuV^ZDPqB7bk>CNxk5b{F8mC z;j?$G6MqWGB};nV;gcXr{}JYpd8USYvVTyO)LKQcq1qYIvW0$;zt`7E1Ja{Q-+~fQ zep=yVN{mV-x<=RvRV^W?S#q}0eh#us4{`^o5{!BcIyDYn668WVJV`JM!5(K zLJJ1(pr7g&Ob>)c!ItTrBxLxYEFo(EH;+NIJy4=8UKrGdQ zzS5+*?LiSB5pRi(qov!chavT#-y4gh793Bjrd{vnjDrvdlD0%2t!zDUd=T24+aZ?6 z8cLc#Z!zu+VQzVLG>1eTMCbij*gNMcTPGqgYgcKFdBYo+x_04w%xI)yd4lkl?$W&m z_YXCAZ58a^s#_AVcQ?WaGeTxnY~W@w7Y!5LjPlG;5w_+%H$gxbyhY9%kUGa92 zS&&Y>x<&ccxpRvKLmU7_t$JTN%rg3EYNCDOG14bk%?Pj!M z8RvPA{mttZ=38qx#m^e%)ax|ba46<-7>4g%ZKmwIg!VXIC`doCR|EdVIyT{UeBpn= zI{)K-^p9AF_dhC7;#X05Gfxl~{ok_2LFoq6f?e95TBw|Y429?j z)?Ic8Jg1i4zjXJ-GbyT1){~|s#De*Err!>}x2Ww1AB!*}8D)o8Z0i7#w;&=CxkZDS zURm{EtH6jA;plry7d=w~x2;Kg6*KC(T!9*vQ)}l2(prU8uu8@F&>Irqz;C3 z8le#~i5f0s=gC~B8|RzV8u@*|vhZvErJHl3`A%i@*&~GdEJl(2`%>J0joqm@xVf7B zLlk1v)>LuTu|9H4lR|5>?DJ!EDkI5vFkrNpw?mC}Nxq=z#1|~|9U@_*%CB#@c5_+WyH&TUUNEhXE({1i zTBLr&LU_lGj${%Y?lV?$OX7`n#5y%+<^3T#yQ*ma(4=d~F@Z$DMAzpRpU>=Hc*PO; zryos}DK~vqDFzIgs?@0TcYEYdBZ{!rdZtlWnp2X}G@swRKEX{BnydiP=5UE=Di2!c z-rrL3CVaJV8357n5u#=k$qlsiMaMR{weUV4^!d9cDTQ#aB(2;c$Jj_-DdF4+giP~Q z=TQDBw=y9`UxVy%1nH4#!P=!CI|}RQJ;>|w+3>x)_M53N8^;+8Ya_1&cqwGMQdjS< zG$_dl5SP&r=u4`1*#M3P&3trxxX6PHT=x@Y4FgdW{n;z5zCiwFrI|jZ4%A86z79Bb z?@i}cBW_aT6k?pm5$b_1L_c+|*w<={xwW*q190SG9Od1Ui%LPH9*|PLpiq*T^ShqA zh;5IQFlH*zTq4^cPX$jIAUj`B(yXtHC^<>w7H}cCLGNSX~qWu0=TjlzDQH7Vjjva3!_qssk^#Z zS2l!~DhKp}$Y2Tg0T)wcrB=RhpW7)`$+MOwrl`Z)hZOmngsfqa&DT<0Q1&fwBQOXM149_s-1b7C3`9kYgBE~@*A3U{k@D{L>jYa~Yo>y(oS4Iv5Z3(BM z=VWF=Rp+=?f)*8)tJZ(Lh6t_F_o-^^2$_R(D13yOJ;IE02L4!>R zz8_ZSmTLJ84=j|SZ#FvMrw`xgGl;KLb^x(V^?0}Ejf&pf{%F7jQP zFO*5g5$9+GqgVJP0B0)RzIqOA-xPKa9mv-eKM&|_q*3Vl?d4804SWw zBRQPOjA-r19bZ0Z-t<<<{yX-wUn~XBRuH2VihroRZw%@aAl)zO^I|3UMU3**d;5NL zL;9^>G31GL@fx=F1zqz7S77ietR?{ z#c1jl8P5wds3!g~5UbS+>eji#14{fw!je%4N0aFGU(PKo(bSI3&kpqYC!y_sX(33N z**gAXj+UaK?SQI@#n0Kq8fyXtjpg`#yN0J?B~xTv1qTI%Nme@%P$PdX&BBBoc9Txw znqTDl3N2=>u3k=~&IeQ(nGFY3Ojr%;Y`#i)0ehJ*zi8bfk3|Jd$h^wN!Y}=ai<1>TJ44$ z#U-{1X$14~8v=ASqm%EQ-|2gwkY zEA50~nwe4c;E3cs@>px1rqfj$Rw14N^AHba8L4WFpea@-h!>(|5jdcpAlYA&>kS!7 z56pxx8FhNMLU0HcJzQ0u#!DD5|nj+C!B2ihSt_#4hVg5AC_i!Cyk+*zEC=c&Gd|qMbn}$^1Yp+_> zd9~ETt^ExwGi(-;7j9J2W1j%x*Rk>1We~I4VSPJKp{1d6-E4AoVaiyJO)nJ(vkszrcc-~OTX#AN}AUuO=6QFAw1IeT%m9XLNZZd6B^6X=Na_C2cm}Z zOiF3VhGaL|lnut8_rA5_xdHNn!eLEFb!dQDYE3fOxO}Zq77&XeXBevclAxJ%N#JH^_9J2yIN8K-1c`ED-igxDOQE9Jh zRXA1?am}^zP2;?52sg8qvJ{rSiZ~lBW67&k4c5T7ClCwZ3Bf@sa2=SZ=9n1mR+0Mi z0e2PRIi54m-nwM2?>#f6@oAEBPD5|+y{y$nIs#X2+#{kr)zdC**e1+jbIf)J-Pa#H zd~3(KGGIy3Nd29z+`FXa1sN3Y=bqy`shWjk2`)}nG4klIhF|*hxy9J$+f`~f*WfZ; zy|${|tidlReBve*DuikjwG|7v_mf5gbPx9$7g@a(ib0zeT+=ETRJf8tv4wjpE27RE z+s~l7XI+wR-FpgLt;hIOZI47}uKm(G2;LTsa*HRHzajiwN^`^|tKnR(Tn&j$$mpD6b}M55N{9aJ=s>r7aD6dPOde24DlWBSg#qrp&Tw?}V2K(Y=z zHX{&Nr^(+rpR*h}%(d+0awP&N>{4M``x}AKWTT2NG*0QTe7!TuFBwwiJL_f*(Kd`h zsPq8UZ-NoJbDlN@P#SEoHpyKPBup1k-FQcGls&G<G5uvk5VdGRZvRQ#T_FQ^o6j%?;jWqtfjkHBbf3AjTA(VTuIMD^Gvu zfSCYpPd!B3+sDZjQaRL>h%AHB zFWT%xiZ}|20Yb2|)>eC_tRXXIY4vS8$BH|c_cr|el0CU|b5noN0>svM+af{|BMAh@ z$2K#LHedOCjs#v`Px3%ukl-xvk<YPmuK7Fr z=B*BA=N;U?k41hz7HZ;b_GK&UBvcgxmXrZJAk z_fUFl@Lt-_zHC=ST6wzEd)i=| zXC4~A_s<=$M9ZYtMkNws@1bt1=prYWz5aRV3CgxkpFb2{0#VRz4v9PW$`4T_%dT1I z7JUox3)cT?daLt3^H(=+J+Cs4qrtC!JUYdD*VmDN}GNV|Kp!}uudB{o$lWa_XHKTz= z2bEQk=`Eat*)cO_IJ{X@Dp-Q0A$pcC$b)D5$W^z{?R{5(1#ZBTqBD*auC26u1Fg>> zoq@ufHNtP-WI4DMmNm&UTJhofnV}AoC4zq(Jtn#>U}#Z)q>z6z+jeEuS_d>i@fyZR@?!w8945payX^-S^@m%nzj6`!Og zv1R<<2fET>9KM_gBDKAP3x34D6e78U3{Rue!j2 zkmAF9`@F$-I+ECJ+kCb@uDA*DCm=(Foz? zk-}OZJ`J%XM!QSCy@yD=?D!_&6V=QMe+FVg32U&=e74JW@m=EOiht2OsqrP{%S}O( z`n5&Gh*cLnT>@t{k7Z1DbEX+z5I7^D`!my$WStzMosQa1ENFao%X@DbmB*|8qYv&8Q`?vebK_^3hY0QO zOFd?;mJX)>xwHSUmql97KDe6b{Gaj|RSFLr5p!H_i=(qdKW|Z>YZ6ujV9il zH^E06UM^RyuYQXDH=l~bUu%#*W@>1x(mUnmF(b%#-#d3)PoJO-(u@#_A%BIsi_X8zK|D;We;u-b*l_vJ+wub0klXZSBo ztBJTva+$bO^kzhp`=U7s&Hj0@>MfJO zEb~?^?x`ScwGw^5H@P!@2rYTlV4S(p^(-uebbT9Q$55{}ddFLDR>I*gl}L&$(xRxt zWlt8@f{lV%J^-t~!eL@b$T+F5npH}GL&TdMe)K0AQ6yrLI-Eh6v%fI zdU;fdVR*PoU7)0)*)7IEPvIt!40P%GS@*?X!{RY6u+1q=oWJ8RNB(3a$-u4Q=fGWv z+b%5?EZ(6hzZx0wi7I0NM|!7pY&-4>MlzaX0|4Zq;pa9=_WGW}ySL25T`TVD$JQuk z3Hvrk-ksoW+FeRdQ=Wh|Tcqj*qar3vI5NZXc@h@{Ex=0GRPmo0?0RX{zqU)2K}pZ` z00!tLezjhEv1MpDJOW`~O>z8lC5O$V+Z?!pR=#p-lRk~zlj10{;BAOQ?pMC6dxz-H zwX$wsX;D7KBC0=S0uE_mUXzLwVzK(*Qup9KAQ@?R-+JuAem!2cvb&aY&UbW#u|5g~ zPJBv9IjHXu`(}}5x7(g_T~GROQVk-;VkEh& z_eKiKNaYsgH+u`;g?2Rjc2Z+iJ?B5qVCK}Ld(8k6qyFBAs*~m6_yo3Q!ZgnTRcX%L z=^D1JuGj?2c|*9o`|arYZ$-DYKVb@ieSLiZU{{%x9*pdGP=U7Mj${x=Dy3F`5@=5) z)Tii`K4&}&i`z)DHfJL9X|Ef^b9HYVx9WH@M81iN^$VPT?? zTTa=RJ_hQ+QRD`T3~5TkD?=VPy7fFJ+i_U}y?&STS*ujTatksROYL)^7ivj+qf8QH z(j+Ix_vq&S-qD+S9g@+8O~=fXGbGOh6Z3G3?v#o|IJ$;~KopEb2l*e#fu`0B1t*aY z30>KkM)0zcA=5EiR+e`!A9)C#>IgSW%a`++CAxie*h!8&ZakaaSp@ANTdMKFLu8rE zN}NE@*}>C~LV%mNal)4Bu73Fk(-9f_U9_%BzkM#BMs)?ZtOhlED+-HYgx`*<9^Glm zSH%?}J}YoZz)MU+$KJ>ZNqY$;d++TQavI|=!4YTm3v1Jw%)8YALSJ+}uK{v(3QWDm z+qzM=T0N9j_un)bk=Yq>nZ34yvw_KmkJ(6*sjOIy#&lW*t1C}F()_j{%X7TI>1HHJL0Cc{mx2#E2So}a~obT3(!PDD{XbSNSS7Dl7q zd{Mk+@0J*OmZQ6yGuVSDV%V_a8zn4iq2|6Yc@$|_ZIR}nR+r{Xh!jy4A&HVZ1I*@! zT&)@h&NISdBiFqE=xp2ckf_BRyrIK-?wnu0H;|*BM>ChX{L+x-rvU>wdg=-d(iy*%X zxA(hmz@i?jq~{1jP@W{pg2K@#5)M=Jy!8k_StNzPU$`pk+NJSwn=8v+I3u#Fo3XV% z(aHzAi=;8*p(ENLX|=3$i)yS*{UyJ2%h}j+w1Pbv^%}}aL3yrF%qu#RfN~IZ-Zc|S zF);}f$vC{ZdO9FOu#G{Io~mP@>piBjb4fCILi^2##&W>rLsaYH{!MYW)w<86X=_4t zlp{lrv8*wbr=+98{853ZgM>yuGG7>xJ6nHVg8@qvJ&c3`nLs!OL#Gmbl7Z4 zc5!;l@O!#Oy=2lH@@KpbUxE*^w5ZwY>njItgR4frDz}$kVF7*$1}Q3IU&gL+Ev=dR zpp#jfIV|j}`t)oFI%SHgk$2pTJB6P)@efmW49dl5uN_kOFx#5t(A(a0yTP8z3K z^C+V{(bPT=|B8ib!!+{eKcT3PZMq*{3s1H z=Oi^FX>@S7yT7})d$=1CPCx=f0uljIfhg|lFj#~$7&Bx1M1MEJzYLKKUbV?&e+q8F zBZ7di{`*|%{~B-je<`BmXrAbzN@9J$F9B*afZv26Docf#X1tYpU`3KG;Zn!I?AAkw zEA2U4>xC;jbbXdz8y;%Hs_yLvI7D9g{9anOUa#gm>$41#!TTwv%*`zhJN&QMU#9{B z-XOYzr2#QKoE;;?4s69`$XDSBSiDE@$AuB4#^ul*sxx2N?az;RIMG;*qzvIwN>Y>u zDyY53*U-5q3X4OG0WW;fzLCY^$;TOZFDii5-Z%arcG0+(l(*QB@v*RR3JUPg}K zBK#e8#Cdr=xY=n=RdDKoN&#urRh-l4Fdj{zJVAf#0cIX`%85=6SFOkjLpCoW`2Gep zRcl+wmlx-kJhO`g_hIjnet-IcAVgP)0~{stpiE_XH_SW|koA##K4A!uVmf?_ zrR}XZD5|9zxKK(d>t^HSl*=o<0>F8uo(Vw>3hC5K*4Z_?M2@xAlH<3$X&IBfa-K%G~mV)R9+-^Kk$v)!}%BZ0huqwKbB+k!s zevx9Ynfx-t9+l2}nikWBI!Gvtca)DjEWw>-X!_Qsy>kr8J|LE z*jX@3;%?2Ca;_*T^$)G&xbRCNAXz9VJQ&1p`~hvFgY@XoCdS-hT?{ebd*v^3GMm7HyOk(2P}y@Xp;|rP8G-wB*ZeUS*`6>{EL_mxj_WdD*i&oU9YP&c zxzu3n^%n3cwkhRwV5qgsVGtSz%{Yw0%F}oN67~DX}k1Y(BKt_GLMzlr zB+*DmE_$cat8p;oIvgA)BC44cn%ntK@8vBt9XMKxQP%>muu@@_35Z8cu8Ct_ehdMI z0tvC)m#bN$*bo08E=~gg9$2oYH1wIk4?B_aTEsq2{H%`T%v9bF*qSX zCktuxA5aSA?r`tX!cuc1kQpFdAFkx`L7^rceJtA*M>s~))zG*mI~@)n|Isc%t=F4> zkE}$$8qUrz9-WerkO>n*em_l~Txvf(E=yWi>)Izz*P)O^@+R?7*AZ^^|55hNLArEX zp75zUg;Ta|+qP}nwsFd~ZQHhO+c;&r>Z^OZ=j*v|Prq{~p2#QiuN|>7ckZ?J`e7lD zZ=aIYD<{o;*~1&bF-Vh)nK^mxBbuYBi)#`osE*sBB@a~i?%FUmB`~*&7w=H`n#O9%qrYijN>Pupd)53>I3W*Gy>u2smP7y8dq|N;>*)cK zF3O#eHbduxVv1~8!L(zW6P>LSu*^vBc@g6zVb;d%9<)h!@mr|s1%B-mByO?8ra@pf zG7a`qxh5wBvc*KXLC3sr_glUR3S4&ufu-|6uu+SN*thBs@EknX>Il|{sQQV#WO4c% zOH5sce#3MKByL8uBi$AfqZGq~RuVhJ0AZ*^LObVeU&s=#OGE64S#!`?lYl(v>4tAe zXMdnOV(17hrg}Os!zXj#aES62**!aI`}K(#s6A8)D#;MnHile{AiX%!vZ+URiPFN* zAu4t;iJZ#Nkm6=7)cqX)!6kmpBV^xXo^Q!O4lwd}-pPuTW~(nvSy$RXGYhTkLnl*K z_3JEpdbxMjzyMSRVi>)|h`c-uqP_I%Ye^qDyt+g-ayecF{E%piSbn^8ayi!_#>4M|;h4p`qfd4=a{w7OsikbYbfqj#rtt{^57nZi=+ZFWtMg{pSz8d<>BZiVjNC{stIp`fyw;At&VNJLcrK}t_{aNzffX& zhgj~3XE2HIJIm@;aI;_u2*E%R+guH?%oJV(V@JQsYyBHu@_@^kbnm$g0P|4SxxVC{ z@f!Zr0LHqtFzE4#C0Iho#H@eid?<@~`wQWG0yW~>we80b5TgGvulR3?*uRxmsDXJd z40e8f+Fuyok$~_g00Slb!UGF+)$bt)fK`s?jsGDWHg0?;Q&+bt^|T{ft6b5vCueTa z>F%1h6fT%72o&wEaaGp2?y|7Fzxep@faO&!^Ol+VMU1x^bNif<`7%9m#ckqw5nl`q zP!gvDjx2I6=+sfpzJdkqr1&(Lx9+0IvKpFYT1*pHW5Cx=0~g7(0>^>8GXM;Ss0F`* zUIGn@zqx3^PF?E{hEfMyhz7d|+_xLWYdcQ*tl7H}dZR+N;5XI{s$!; zV0C4vD(PHh)FKn(t8&6xr&lOeW9?-N#-OI=p@sIT&U)QZpEGFF0OlFE5IfXjW7toY zsKQ2JENcrLjcsvp43XF}2+%z{dGb=N*|jBkjpZYoG8>If0+;Q^nP-m0g8Z-d5@S9G$sclihV)Rkclp z-h~2jIm0;8RH%`wiZOCi80XcI;;DK|l zDy1M)N;!r7^svygMaf3Edb^Yht~H3_dAvH= zSa`*WM*A>P!Aupf{j01Zb`I2`iRhu5B}NrQc}F>c>_YpFGgtQfonCsc12y&y%N*!q z6i)iOOu6k+2eCK2#DiuG3r|7tkc&pC{BC^*^%J6Tk^y~-k-4t=t9cI%*CT6xcsq*8 zW{&yuL8Zip1B0_&^q^KDh#-~MNGnym`i?6UXtMbv0G9ZLuAaS6mgID}d|m6MeI4_{ zDJ^>ffmLC?&6WgdaI6{P+d|z4xXY)k;3u%FQyrnt!rFFBa^x2CHQlJ`ut%Y&fM#}k zje$~wfBnht6!y)|97QHTOqL4T5LN-W<;!LTE_ROzj0pr`SkK##Ei8gFGGh=d$?PIb z{I)gdpdP9=R8+;`R`DU#M>aN4<_?A1%pl-MgwPn3D4x8Bt^d#(5}2mCi-d^;tFr=G zN+D@72nJoZEe&J~iXUeHq4Q$O`XvZ&hfXS=z%BO=FvE zT(({HM~M_&t{~ORIEiYF6+j2sdDq-%mu8NQ{rpmPV7nZObcOdO%CI|B*0|1Ht6^Sx zxH!<_hKT4f85sD+FdIw5D)@zhmBzp_8ZdKUvt)L&MZ0wxszOji@UTw`k=0lsF66fb zZbk#uxt!V?#WVRCBtv#SD1;8fxDH9PI-gXO_q#}H=x5dmtES9$PbE3GHGx2anT?5BI;E@^a#!RPslJOG6*HTtiDd8lXvLmF zSU2bs`Sly>_k-q!@Q7T(*2RZ$5{lEixl!XIwVBp^%ixk_xU`>VG;ezBtQ-0mQ^lPlghC!CAs%9;jX@ zXs}=j?xN7ZFz_p6&?&el<^zdazVuCp9vSNJ=UIr4&_0VKZmz492qzo_(5RDistglq zcYnA0wZ{8apXC1iZ`l=9$4a-{=MEo-jYyu@TtOMiwn>{}h;VBNl1j&X{=4aCm_wJZ z)F|sAY=hfwsg;jFLMOcIbl!Y8VzcPbo8LDoz;D0XTA8yp(tZ*0TPiW*Xkop4HG~Wj z$V01WBwyQS&L>Aap(0f`6KdFx6kN}pN3E<&*1;twsk@#z)hEufO~OY{Y01-OoaOiW zOr@rd3Ffb98jm89De?{*I{qN4u=|lJ4q@JSb{4t4Ats$6DIxBopL!WA=9I1Ns>&l zd0Bp{=3IFM#n4oWfhkX(EF5W&yiJ1BvD9jG^f@FMNR8j>jCfkphKfz8*~LEX7AtY) z5K;-q3jtTF%r-;ln1Q;mP8v6-(qwdX=xK^>(WDRyW{98MMBP5C>s73>m8eX%sUS*8 zggk((mtl|FuRe;pOIU?ajR_Lzu2N8Est?t@wDh^i=mH?5BR})$03z48Ll7AF?CqfXslkO#^ zWd77Hf3Z)+N-mkIs_u%BxN|W>IguCfl_&Lu=uwld*hUGPo?b4%Iy<+9^y@QI>dkQT zuW+Mw(4=$L^NU$U&}2>FXbC}!#84n3BMb&0nII+!=P{%Ri5JfJo1q(_VWJL5sn9c+ z-<}$zaE8iVuJpr`KBV(RBiA!!wMAz^kC$5Zjkng?MH{`lAnXXU5vVCS6L0Z51!CG- zb1*W)5o-3M)@AXfvRrqn_Wqj3v1fG`iG;_VhnjA*A7(Svz($)15qE|7Ic zrhOTxrM7?U3$iWtZg^S1JR{WvtdpwCS*pFG&tq+BBG;yXocC0Xhw&7kmLKCwOAmW% zytx##3>yOtWD7X&G?2+BFrn`}}<&8;8$bprR>f05q%Ive+59IDL zXoI$q9mG-Cn>o23Bcj^MW~003Y_Ja*veA%It=;%#x!`y(SSIIFwRyBytH*EZgsgX^ zN@OYY7s99FMjPoKkwid@L{AUMi0Q3P;OqmID8km}k);{Qj$?9eeoaJm_zsUlf?}{J zO{Jsyv}7y!=3hBlyJaQP27>MK3%G+j0`}7U8&mV(H z*_(?NI9ntDK{MZk6T4q&gPD>mmHLPVQ<7Yo$M%h?*(~y2Kq5e_O^@%ya-LATffOMg zk0@qJD^M-xHy^$gN_lG}yI_TIM7HNm%tzHms{G+^OfZ6DA?^CaS&OU zTVL_!Z8$?o0c1BtqYH=RN+DuQ=Gh`uh*HjPREBMOo`QjNgp73&pvy-`4KtdV_wz>2aX8HYBr~fL--I6A4nc-dGHk`=K4kg!$rFDer2aj@44g>a z{)d#{hGu-d>o3c6bNkL#0bLHqbV;T9aNucu2dOL;n!qV^l|mMqUtDS>cF7)_Z+2o* zUD*9h*fvqiiTu^e3<0jp%!-JU_eW0zWMrB}M_JzIjz{xFPy6HyG`DR?dlvF7RYg4- zJChg#kz0O<*2+B1ipr$s%_agDh1Zqn<#QT2Y%LrR0xdik<}ZAGYGj;wN6`*h*?kCM zi66n%!-zJ$4X5v05Y21eVe8n9S~JnbZ+4`C$E|(!G=qfh;6} zF2b7o=H#|`+=K0|(d1Qo4-cSA7waQ%>$GZd_GSkz;=I@v|wnx2AEeJS!!K6I| zE510p!;ZD3^gnreWz68w39b02yoE_;>veXL$9gh8etyEOjvON$WM5B@%z}K9@Bc6* z$aIQGemF4y`O+Gbp(%*X9QR6Gr5S7*m19s@+gh&Jq^C0wi zHdqC&KRqP=i?DSiOvix3TV{l3-+rt^;npq?aZjAA7sZhI!VS=Q6i|~Y{8~s`vnscs z-L7+g@SA3k;6a`L_UR6bvmNwu`0>0SBJ-7gd-`ogmf83FLdcFSabNY4PA>dn1gGO*vK5nEEn#)!#l3+z(F;7d1`@5;1xC5J=oVt&}%54E?o!nTv2y z4Klrv=>@5E>m83xXeR&Ue*Bb-jo}SaCUjV>3xjIw9ez4y!SIxIK59!%K=@i$$U0QS zpDbB3EAH^%Y-2YvV{$5~!nSF};_~C(7rdH-=)r$)o}{z&Alnh+t&oieI543+h{xihXxMpf$$7!t+`3j4N8c@;Mn-+!#PU4p)`rq->gf{7B28y62oiOBunD1RBeJyGr=Y| z#7IY!C`xr=?r6W=)-JU1v3IDewxz(oO((X7!(4a_>}V)fepX1s%p!G5d}0ejU=vM8 z)2(5P0KTDOd?Yyg*mL(zToYabJD@!}n8fG^xPr*+B4PGCwcb)g)5&lOKxYqDUt@mu zh<-q5!?I`>NNeXYYZqZ_$AWi^N#3MU4xZHk+KiZYkWIQuFz;BcM!1lEwRgOAl5l{v zLtKqFKJ!-f2j4P?_o`E?Eexc)j)!h}C4{GfK_Lmfe83wWbyd01?Gq2Rh(3-pzb*o( zUL1x~JdGDt5G1mT*1?P&3fL2IiC69YX4}f(Yc-}*I1R~_VK5E0P!?d{wJVC#K%VT@ z#30r{a&A@bIw0&|8}pRR1P&m@pO7pw1n~Apl9VgIwu{x+{2^J9pE^TsAW4CfkdAG` ziR-a;YTtaGtdTw2U>`%l5ez?e%Qn=-rsI=s zP2=>XN6wEk`R=#FM1WOGT$|nAO4C%bX4#TpaSZG|LYDbtmMXc2(9r(njk|8NIU8fQu7DmOs~PCoOyV zExYvZ{XFcDw~Tw`o!=3Z>0`Va#;2X;PvUe=-lyh7BwTNAQ(-5%`3<7G|5^6}{|3i? z{BW~%;rQpxfBvETweIC;uV?LGY-4XlqofG- za=u48`o5_C=aK$NCsX`i1OKgZ!z-E&`rBqe@R-0)*TBHcAfNdhfKsYLl9KR#+!#+n zS@f|Qd#dlJD=7h?x~U#RD%qCA(M`uDfME)}a^<5RMXtGp_->|U>z@s^^9sW$9))&P z!4#%j^1wtZswkuU#ArHv$}YsXZCkyS=PJ9;O@MOBHV)F z%e{mxjB)~gAJ~ABFMd2dKSNW|H%QpOn#kpWAi^Ym&;1{^SB(Gu{Dt)lzWue`{-&__ zU+|-}Fi7XWz>jfx108z}?|GVD#~A~Ad$6c1jU!Zat%Thrahblcv3$4`wUlx^By==d z+dO0xOsxT#zNxW%Ai>C7G3LS`mmLfC(;a(p104s7!XQ>qQaJclO~!^h;8QeI;?hpz z6JkaNhPwK?dOraEfg&=4IKzhfetT-t)d2nlKV)+^;5+%AckFld^#3;Z|MVmM_m)7S zESF3_Ev(ma5mW$wuD+nCq$eaDM@V6w!T|r`CYTDXz5b~4i;koy*` zHWwF}r_26qohzIl_L9Hc`sD%(%djiSl@IQGO76rOql z#CFN8P}VTC)U&>R=xL?|T`4<}55s(=c163iSFnk$-^vD}LQ(m&W7%&CqQ9mO%Z@3?{r{dQZSs9w8uMTGK+gRECktmr;0&gf;Q1qezMh7`tt zEHyCilkFn^aDfM^U86^=(oewvx+zKCN{ChT74xF@DrH8~PSn2%oy^-X6iphF!eF*X zD~M^Or31Z9))rJXurD`3|zloE%3pn3FCXdl}ST#Y(K@{6*5c%zQ+f zJ%~lQGJ;!vr5Bp}ta3Q7T1)4@N_nZ@I4mhcw4Q5m{SvE-igPJc+Z3YdHCngt-~=zP zE!;70L-aHI8sWF@NDaEf$XrYEpcfZ-(w|6Ka|%-odf)L*Vi+PDH+>x%$d{n|!SVQf z+_Q<{DFL9Up+QVz>g~)FBprr+G4r=piGJ%43^tojqlJ z6Xq7jP$H%mmcUE_)I7N9SYCXw%6j`>eID>)-i5ZX_iZntG zs0eJHIhk5wv;TF0%T^eP4Sw$xLf={q`u};a(I1YfaeUEbz%bzYEgP+Joyprl>rEMe*{-z5MIQWh)KHZJuh7aqsC2(rugL-M{2N8kbeUZ z;2&^F8e6SiD;3(@a6L1_Dv5^j5(77E2~{-kUs5!Vin~-wtiI;ZPbcm{fT+%C1RcZ5 z5{Zo(2*Q=QyYnTWkd3tk!6K03sWl>PHGNdaMzhCp))r?+O~OmhCQx+M`=QapD>;jf zVO6>%!$Mv`4TqqmC$^*E+e3hiruzuW)w1Q=UqQiF zJO&T&Nc>&8cm1e(`!s!AV^=NY(IWuaGxFaJccA`tr6k?rxAA^&g%{r{_Wx$3_`Nv( zGd~eFGqN;PHghohj-`r5f2(9y(6E8$L*@GUi^?)LP@bw#zfp)fQE3};sDRE&pPx|* zBhO1C37~m(in#q}w*&&lAn0!&p0Vf2^QMBC^`8ve-pLMA8EBuMZy%t%5Y?zsRc)1YBP!cZjI{x1~%?lFRS{cM4Yt>-2%F{YOYZ6fNxO zeLsU1(HfEf^+RBW3UcOJk#Z{Ev>6Du+52LzP#?`*Bsb)$T)^s-uh2(y$s&R3K#*%% zLQs>S3i?T>V`6kNee{AZgu_Kmtt+}71Fj2*uw-aEN}Y;|u*A0E`X9x-w09hj_zQiR zI45vJMIa|J5GSNg)5+-!gEYNieH#KM(FT};CK0|{jAf0DuSgpFj2`XhhOrEK?|^_L zJ5-(~@;wj0!*C}lUBeI)SFTdtF0 zNLc4hiZK3@D#(~!nrAB>iKA-vO+tC3yYTKr!d%m#v#@ z=7=TP8(BjHn9aX5nhf%?Q1B|Tnfdw`rkQ5k^A7j-Mke#UME{#z_J1wUf7!_Xy-=MP zyDbjLhd9(d`><`Cs$3NFjm-j6^SK1!@~cR&jBZU=!ORE z&NL8~8AzxwB3|;v#OokUFj{!r*!Ha2D&KlgGVp#=5tzL~t~w7Juv)cu!|aGrNmaOG zRY%}kVjwr6QaaEu^%uTP7eAtktEa%JDCnWm_xkNVN2iak2sH*knlm=4IjQ%%Qlgq$ zV_whfi`(-c<`dE?YNCa%zf7Q*AFjCEXaesE2lcVz#R2orx)2rPix)tPYQ-8i98a%8 zjLDdFN!NBb)<$Bj!I-Tk0HBZLinW5D66MNx6BB828rVpc&}<^%v0dbgb6G6G)v6yb z&Smxe#l05-5jS<=`wh)~Q{3qOCfrzC+c^Hy!S~mr*B+hD&~;q@nk!8h&;T8Tj&WcF z3F@>b9Z~ezCNrr=`~^olu8}1AFu6y*7PIpt>WaoRL!j?KL(sXlj$Co3nF-YcOGG$;JO5SnIB88lxz`{i(Jd>ouYzOCe zo*5RxJcBLP8M^#5nYeFVpeay&38NIe!npyrgycoC1_3`98tOOF^WQlgShMhspp*)v zmA`x{#)NlrxC|cYNOu?e)IWf=fq{1_>1>!v-H5xX+>bB41W6XVo$RBSI zL2y~;4pRkaiebod5K|q~6(qRHD<>)>P+JcHhTKlk`t^=Hpti=qkyX#zmk1nMRoS~} zlDpVxkm?-@Le2Xn$TFIbg5Y??3)p7gc#v~E{H3oD>CWtl_4n>}@%`=4{?FgdUyt@Z)`bDe($5$cZr*iMK(YCI{r(@y*4*QNnWj74FqN_}tfR29h-W z!2x=3p#I-=HO#1mBjYpuD1}q~9Ac^!fnC4;;LG|*7iPSNO4W92WuT_8SrNC=ACpHY#--i= zrS;bX1K*zecP5qe?W@7@Kflm_30MD5=SyXaf7k?hOOF&1LXcIe!jX28iM0$2X;U^R z4&;?Gn({R)tP@k7((Cn~ThDawv2?#EnN(yx|D&JCpL;eF8D@aF6g~Ed3@^4Tw&}Z$ z8q(uZ zyuS|KTHO!ke0mnQdhR@qa>-!1aujgeAJN11XTkyPwjIlpXlR}8<4#ZaHJVJ`oZK8| z>ffcjUd4}oN0~H362^+-gZJ}dhVlv0Qqpb~Hvml)(E|GOy-ZtzIK4UXJxoPF-c((z ze8%-y7F`V(HUojs(}&1u#_)9rVAjjmLZf<;DNt68MjTrdDNNctS6wKgysf&q+f%et z!Ez+1dCd19eo+s_PrM2FiXP7N5J8U7Yckss*HnQ^K2{h2UZJJ*7=m9JsN>pwYqYgo zfCS}81erA#=$@#29Y};ms%p!lPFXZ1)*^q7Q9vuQ!n zPMNxo|ApoRfFs_?QZh52Y`~%1Du*OWG4cRXs+0og^6GaXeGhjjng_hM{M(r_Id!GB zLppU8C)oClb+zfG6JR@(Q? z9D`Iw_>PRzzDSh~sCUM&hEE+~RF6zUFkO!mB%O3Ah9p>qN z$|x*DDWkK#H2fL9TTz}I8$c_;0p$Ejx14`OY0P+}%tWNnu)e`fH{AopC~EJO=(WQk zTh)yN73Yi~@hsf(F#vx_t`DXCK=Kk~Wk{VdUGY8QGf#iUm??mpI-QZ1&za(ua^@hS zjrOq@9!ZL_MxKm26q$)}Q)v26EOu((F4v-{L&a64&?a#p4XS8r=?zoErVXJY zwa_ydz8pYO<($8a5*>x-RF{op%9TT$WvOcy=qWIn((JQi2Vbz{Hkr%YDnlb(Mf{Td ze&U^m_#us%C+Z0%^Ql*AWAqYt740~`D!1DE-4}S@8kaP$j$|$nRbfh{xu>!SQOrtt z(!?Nm+Fb}@u$KbZy+v{!m2c&AiglMt4{QI0&G{=XCKviUXlZ@#b$^ra{WEaM8~wX3 z{D}%$(Q~|rT*-b~F`K}O2}mblIOE7kpFbjTRt5d>(prL0vV$&tHXSwQlO9N3Eb*=N z^P;(*^CMh}Fk(tzaomrck2V}Gxh~$HUk)*Ru-yZsaS=$cIW7mDgr(s_aj}DEOC3(a zS-pt{Ya_Bod4=)82oMBGGea15pYc;zX+%ioq$E)THi^(#A`Hi_HLRj!-!Lp>;NVH| z89^l&@wNZ#bK{Z35r%<9vi!!+zK&%{BbMJC>Pn%FdJT|(>FANbhF8B>y#M&X+nq(9 z5!W*&)rTkqq$fqF8`X$TXGMJv9^UAfl{TwFiVCtE}U!BuRYi|j6SKF zPiVDBg*j5if4aG5a`veCID*A>vxY;FN92&;7|oIOvgobFuiR$}5$9Br&S>UP^8=js zCvjR@vt#d@whN%@BU{yaCYrhP6UW3QXv&bvMnhGF?j)vffJV782s*6L?~da!^$&Kbiaa@`^#Z8BAUq!+c$MAI-s zquVj>a>`$;|9#NF#5UiqBMaXOn!ibv{w1>hM@8#zD*utnrw$kOInA+Z2A1S}u*^dp{1%Ap+ily)dU1gEnofysQw7P> z>*K(tna(|j>`V|oMmc%pw{)jQjzzEttlHpy`C2gyu@S^FoLlDoDG}2Z!OFrGqn>$~ zYHZlX;Zw!UsI|{J1#gIE-cjQ04=a<=RS@^aX6i8WZ5KK7BUExV)vx$%sHqz*6UU+e zynK6vAfh$QppbRAA~02;lVv$)zF;W*u2a~U1QX#UwT_(q_On|EIowY zW8UxqeSVpO*isz35haFIX*v%(pv9h6YlZ;0QY0V{uDMACdtX$^nLjV8ghfy&4~5U z#NNqLtpXqvK3<_apP&>fT;J=So3y5$+*r5^ zvC^PuJ7j1jMBi!gyI~(=#6BtCGG6FT3NLA*!);L%0zK=nojmV7^M0v#RA`uGCj5-) zS4!$MhFV=_k~Lc(1K^VqfD~I*Dk^<^pk)#>*@#T-gXqX~9lQYncgHbf1~5mFbZT^J z(M_%Y;!hjI5A`rRBTRxzj}Ys$Rtz8e-h^SH3kB$Zh_mLAfuXeK5b z7cG6AoJ{qF2s=0{3Bwhq(*&DaG$)zDpB43ar9`K@7E-Ney&@MZ6n3-v13vP~k=W>} z!-6;${WYg4jL{6WvC(KVR{$In3LOGA<(lv6K?UsXXxWm>fRj;zwx*QCZ->ZNBVV8M zi}gQAfDUUF>>A^yZo)R_=CI~G?s24IP{p7vqI3)Sk+Dxo+u`7@I%;lA)|a4G21zSx z<^mHSi3%!!PHmBJN#y)Fcg~ZW#bqN;S2a;k(P?qO(*^s^3>X$n_to;WPF>+DM%13s zlW~LW84Pu9iz_0I-^ZQ=5gvF~NB_Rg$|!l6=g=UPYlXBc8Nv<}(_z-lg8i@#tF;@4 zHiJo>{ir!wL*w`IMVUmMu}?SQev7(L5bA6AwPx-?3+jDtF}D~HZ0W-V)OT<2c z#HH*wA%>Hctg{3Ax|VHSPzeqZt+R|ohI)F#V11^;R`-?au!b^Dk~juRE%q1oF!Zef z{m7|@MXX+UlDPf5CKfAqWCwcEaXI3+czXNzAd68y4m=#n0y*dSn?B1qM+pw~Rbvo2 zj;c#~=pUT3B9_FVx6!FEmqx1om|6aw4h4JUxS-@+OsmyCOU4_%9C$U5FBZf`6iwG} z&F|vLEgJio;VbA@T~QY|Rc9-9N~14W*>+!;avS;28aLG9DCR^DKnvpL)_$0K>xf<~ z{Fui~ge5PyrECE)ap*re;EYFnG;XHwA0wSU;L_W}LI!CF78+rHBdl{I&mib}TgAmd zk5yugyXu<5!8kQNHV}uMQ0&b%d2@%c$`6Rz(4xO1+#8)|;rE}6(GL~p^$KyaVM8I) zxdEqp2$&;5Rci88u#l3e5Q)pl+~I*`-3YzXPf5!O*0G>oQR5>(QRAWb5ZZeP+)SC4 zkfFD^L2cnv-v{l_+QQ+42B8qfCC(Y3(U2l$tLApf5QL(war$StoP161)Zd?SKJLSF4=)-y?%q8Esz^kPq1+I?zE0nNC7+(r ze)h6z_NlnO(lox3t9IUBb_7=MV?;=yehspWV&IM?-%xqBBZ?)<{e-@G z*BgngimcyMmNxOY`9+0_+oUJCW2ep{JVg?FXiMMU94w~p;uj(_mq)h86=;DekYpRg zn%%2&Ls`i!Q7}Dg?#~+Ez%Gvu$zjD?{wlSR$Ym?b3E@9l2dn&rC_Xm3R%E;L)eVsS3DSc|f5fqKdz4y)(4#dIsgHg4B^-Rx29!LHWk2 zVnboT|J=zASJpyy`}MELL)$D>ebP6kz59Lrw;r4SRoeaELD;_u<^KZWAN~&@{(tC* zny<5akaklEV2jUjywpQ5UmXcq8(nzen`C19L5c zHVxfa%CDhq+~x4m<>36de1*dUz$iOw4fAdq=BOs3@+_+U&SYyDw>W&ucU2+6nCAbnDsMVu89 zy-l{s4#*)}zr*^{Nv0Xct&lBc1AHehHW8>^VLtzACZxyvO%e8+E#qw0fg7RoGqCD= zINbI-E@~kv4Z*NUU?qzs%}r+YQdh1l*R$UqNxb6W-7aGQQo;}aIAp^aE7lCBo)*)( zrPv`5OG|n!G?%IM-?|idPG#A070Dg)`xMOG`+VRyG^*NC4AiQaN1Jq%(9V-Tz}EV| zyDVRa#2U>Q2%Gt5^%z-&rbU!)9Mv16&i($&!V(Xr6EA`M@uLUr--3Jpe}F_$&%xqv zzNti2FjvJzq)!=RVu2t%9YfWh_=ZG)=z4wlU>dk!h_I3+J}P}>dT+UovEXeqBX=iTkTg{-`1|6k&{FJGBK0=embCUo0OQ{*nO1b_%Qsn@)8E3$;Qs9!BmT7sZf8-f8G69x4RcN2uf`cqAP^q^sT{?4$8vtL{ zN2oe5TFaJ+#qiJ|f~7(LMdj^1XhZht!epT0+oq-S+x14Y(d;QD_8_oKJe(qB2uYPW z;dTIz^pp06F!xu0fu1P1r@%Qx#XdDd}BfXzl{cg`vYDACF!HT}`WJ^Ae6juFf;C##MG2zs&x0Eb3SKos_RM+w9 zmy%0U6FMEs7$@m+1X92nz8n@QrVqF=^USQCpnAx7UYFl`a}zm@v3?SH2GzCk+tJVs zU@{t)nG_Zmg*e#^ZGG^(bPpyBIbHLzdwJS4t>01Xa{1iD5g15{oQEWB-N`jtf|gGp zxd;9(dX&7SBTm&K`Cge^zMh8UuO{_W^o(@U-hlI~ppsU`H8iu)v4>m6U!?NwXOdJz6GME$T((ga?SaHeuG( z8&C5`{e>;>_edk;_GM1Vm@!g{!Gs?IMJP-FGUq5$R%H%N!nmz^=?U})0F4ubVNVK0 z@FCYvE5ItXpJ|m__v12XdFM)_hu zxLs)3)=-viN_r4V@+o!$QZfTlrJOr`Qx(s;>d;HAqMH<_eUhnIz+zwC<l~9YfNwGAv zuH#-Oa^^f3DeH}$-bi!{J;*zLxFnNoiZ<^3XfK~@AQrUV$p_yiSy|c+l=8|Ms%w#x zxxLoJV5p_Ux&#y+`=@y5#=a}-rp6p%7B_n&EeeBIdriVOi@qsvbe$^( z6fdRjI5SIJglo7^jFC>l9>JBBZ7|-a)y2UP9+la=@())=j+L>}42V^%Bnxu0h<`>x zx4p0(EgU^S^G2s;rxC~A=GUc~k~Z)?cz4cbV}9Ce%=%+v4hS%hvY?uzOW+$vJTNh1 zwOKAMp=Lg_VauI)>)oX*7fL~j+?I@$E$i&cP3{<+8Vu{LZ9X?Tp{iqs+5L_#BO{bP z`ebqK^A~uwLT*C=o#2(8@J@~@TM|yAuqX7crjAF=`u5o3EWB~D$XzD?^_jEqj`%s! zwXnpGF3T*}{}pk(c#X0Ugz#Y3&V5;{r)ZOso=2tKQHBn^OLHFibpNTuQZ4X|0at}# z*3h}xw}BDXD&RWLy=L6DE&l_%kH-|aeK}~VHmaS1QSED>+wo-kG;SQI+E{AxT+O=U ztk=U`cl_$y6wkiXRY%JU24}@h9p{$|x-zSZ>kS_FTy8X3W%;n5wx@adSH!T)dQKFo zfo?+yl=c?6qvCA{;t&G_+5YQDI~3v?rL-NKZd^6#dJ$8VrX`d|AurfVj%x^!7v{iW z7`eT$cQ_pLi4Wv#fy&s-#-277EsL8`i&8zk-&AeLDDYXHHd-OcmN?oZSTAn@Ej$y% zMQy!lNO)ahbrnuR>~E}IRoZ@&Rttu zs@?2RHYN=q? z6_L(EeoM+b853(x_Hdv>_&DxjRYxk=AlL1^YGXN^tDEC#9sL!h2NQOvGGv*pk$ke7cjDp*DfUz#NeRSuk|@R} zF0l`pyk;oGNsjp)bfIYd(Na6Ck1q%****9sfD-O+v^4wuoRoug2{7p9IbnQR(YdOk zILNuM5-atZN$Q`l^%=h%*O*f$6(M&<9_Kor<-Jz5Elp2^4e}X6Y2Gr44#6`1xV&ga z>pF~bp?Xmf8%lC=4G#eMZPKE*&n}gOiNpW8spblW&ZY=$iKezZ9a<7*_pajEPOiq% z5!;d1@di|J)Qj1+YV^GP>2Y5XRqr|l^hI5_)wDluW}XscGps_z(rm(Cvk48L&#M}ek0{ME-nhRJp|oSA^Q@_cpY`nCzhZs(+ZWqn!0O_|BIR4wm1i^ z*kRCEecsQ(t*y)FiUPKdzw}EpHAH<*olB#`@IEMXKWaKqEJfXwWuj*QxKbj>m4v{} zi6HiClhAq5X`dZxC{Y>n#Jc#DTQ(`^CqnE{vukL7cWmhjy-pfjFtd z0TvGLA$#%wC+{i;I@hoWpRzJJ8TSs9e*F+zWzNfRrj%dxB%6Iuco$BdR0>M>8 z#nlR>$Z4O8{4Kh>M`g!>l~?=6Rl96CF^Z|`V4GX{WLg&!n|WMKVaRniJ( z4!%x&*r3rf>nA4G25_~`AjlgCHmTG|jfGsL9fib*D9Gg1Jsi9J0yUzGfsw&jKHOeD zg;B)gFEo>2?ftBT8_+gdG7($-OWH-~^&Do`%B1P>+sR>dHti#Z>N}&{u2HV>0^e<@ zI`!=og0}2|5zF6Ye;J5M+ z`^z}Kdn~KGIN1t8hf!*3w9RN;$zxrk+EG@LV-wg_$!ZnAsh#Z&CQpj|-M<7u#|_~G7Y&c`Edg86au(I1&t718R1P~a;NK{;ies?AQHS|?P=upj1u;cqB_Md z-6C5(GAX)xC%VL-Yz_yaPzn)OwMLb`RneIxD*Q$D?dYniB#g@Jch&*TmI(}a4(;3? z?UJ2i&yE>f4EtiCfjrnibVBd<3M>HGLcP$JQ~iZubhhe`(M`r(GNUl0cX(>I1epFZ zbuA1s)%#=^?WC1&kDBQNbC}N=%B*Y^b4sjZPN!2^wjAv{j*8~+hFc}?93$Rqd%_l} z^qI$$To#iJTU^u;y>dVW>-1*UL#eH$eR>keBm5EqaMpUg>>iH@TN*)d_XY!@r*YW= zAM>vpG6w@RP1z$ol!f@?6_tw*JpZUG{GSyUnEx{dZ}#1({oj#Y0N7NgaedQhufNY% zy#JTs{ulc2&;Eu6Mz)Usq)q;>X54>s(yC0%@L)#IDkIjx?`JZ9R=zZ*6{l z-rb@6sNj?Hky3C-n97GYZ9AFR)`Sz=Jo)ynb83HY z?Rwv@>ils316_A_uWR+{)%daW!i=FSiknN{8i2?@LP1W{THmK=sd{!623XO_dZv?Y zk0-6Z5OI2#q3keZfIPW*uIfQ0>xA z9(!a`edG5J^MiQQC9|+P<<$#k88bge$d7n{a1P5@jr(v)?Fwm=v))8kP|*;5R8EuK^*#%XI&_)7?vJeMK_(jOItV46`hn;@9)b?KHF|%4HsV;2yFGq62vNi)1l_ z;UI#~5POh~iP8tQ0DCi*-oja4h3V~=~{f}v0aV}yjNSx3hry+bfVC*fl^w7b4K+<2`0O35u7pnXXHYt8mkU0Je z(Fn~v&mwD^o{lht*2rBp_s8nkrZ_(WSRe+1u-~7om$omS;4xo+XBc`jt1oSjfOyYE zUJxz9xpRSRc?>PdJTbd}2iXGbW(+=q>fXPY$_c)hVs4REWW;{+7D=a9_zriLVdTA0 z*#c4h?1LUH^isvp#aU6kE%U)uksW+5!_>v3nzR}A2J!E-Vj@D3MeQf0sQ5{=vHwH9 z{(GK^*cupGn;8F1nr21+-@B}HzHBxp(9XNlMQF6dgixB`XJWV_*kUuKAyzYBxAHSB z=7=c5Zcf{;pXSWFo?r<2(y7$B8X0vQ2_6H^{VEx%G9%Y@$=2l!=7tI<4CqWN6nGMG zEd+f9ztJa2)G*h?f>ggC)ok|h;aFc}b==?i z)cX$Qv!W0W6gj|Q+1l!@1HsT4{QPqjr0eEp{6PObG>S!Al6;?~?C4Xb$@34P`LD|L zX=UwdVQk{~{|F-!$G_bV$WmTW{8Vi6;symU5P*WxL0Kk}(DWZ*4Uz`tA)?2k<5x^h z^N$sfv(6bjLAt#mm4nZG9W&z_y>PNT&1kjBigN1u6~)``BzT{Z|nE62ipGq}WWMy>~_ zTn$u2iL!_|fP`2to0kZI)3RAAm=*N1<(052a`%4`O4v3O!5adGn)-AlRhbzWB~;7s-aEFN1_Lra$rxrYRs+ix5rrD66nU%i!WfMJO-{Z0bwAFT1OL5ng2<`&#+ zuMi_pUHNw5O}KB^7;IKYmSBG-Ou`XBAea$i^(|1&9%kmk|QWRO=_?rH)Pf=vzL1|_&nH7cQ6aFfAem5 zFGFDV_U~ZB#)NZKl!$E6|9iGR_<;^>eQInzK9|(E{vlfhjE#j&tWC@eoK61oTwVLj zRRrFIAT0n=UI_x?VCaaW20u7yxFA$)nSY8fjZbd!1cE4gedz|=uj0o8dUeu(>ws^U z`7~Z=!j!F0?JD)WymQ4i2r4Ln`+FiF*Tt5S#mShS31+t81d-wxgbkw8k;!P_(`W+3 z>4^)I2#Q3QL-aiijDkd9Ifwq9lRtZ>n+UbHX&vo0==KY!G075IJGi_yn2a^(-J@go zZ$I1%&YdgVo^e!yF z?5RB+%17rA*9Gh_njKOIiJlI^W2?->6PTSzxjdx z<(OQ?RZxUAuyiLBj=nOK%zk4W2wJbKQKEq1W>Rw4NMkGd4YXM99bhxVil5i}L%3B~asmCM?JYT)}N`~oXywfWn(!U;IFF?B5Yn*J6 zQ8z>_tNHx_L@m9BQdIpe!^am1xfpxou(S#5iU<-@@%3*Df%JsdiTDO^1-ecb`Usse zlkJ`@KLvi(#BFAA6mg3yA(ohX@78IbK53x5uZbSkyoAcA^;s8UgH07qucJDF&VKer zV-QiDZqANFy>7J}Kynu#zlu-o%Iorq`i}BTuVCv5J6yL2V8H8PGnlvcI@yaJC(ES7j0{@gtMn)!1PXE>a{%0=z2Pi%%x>piR008`$U$t2Z-|Y0P zNr89Q;StZ3r=V(}kHwucSzTE%F-3W{^ZTNXA}tA zx3N{ct19jO%EuxQGtCzV%^p0!uD7tWdmk)C$iq86WzCsr*;+}O@!{PPy4ga6{f37) zD{{BpC>>=p4rKl_AV(8IkW1(`ErLBy3W#OX`Kq?{gw$VDWX=lx+}Vrx?}dpFm;v(m z8A#;MF(%tT1=HAA!q&q1e+H8AZva-3(wgn(f+254fy8dAE@%V4q9>vMZZgL{nLGlN z2!RF+)J$3RZw~Cb->0hgY=Pa|=u`p`?!UnvDF!MiL#0Gaq_Q{CvY4F?D(+93r}RJ? z?4xpNTkqp@(YwNhx8@uO5^}?bvoS72H~V{gQUvWjBClI&H9(EjpTSVeQoVFof5!|7 zTH&R{nyJ5{uY2N3g97vL!U|@JHao2+xPv;$DO_{5TN6;%U+DkHI)xZaa59bzvCzOm zC|jc_P!WeH0U(%>`tKq$u`*Z`&uJxUy#@jKE&`ZQ3X+jIN3PKxw5HvHaFq{f@9R0@* z;UADpLbOJMfJVz*mPw2wdWA2I*zr7oadO-pDl@4i8>iq=LUmiH>%50%d7u@=Fk^;_%kQ<*%Uh_+3#5BO5Q6oeB>Ul{BL z&BQFJ+DG_*|G{Q$eO_-rajNj=xt0H*^C|snr^x6pzuDjXWS^?%e_bBy1PdTJp+Z9x zK@e=PK_aS6URA9?A+}s4NFHb~Uu#*B?&~pSKbI|en}3gbF7?aJX0t!$y!Y}G^plp` zG6q8w-{E{?YUjQ=&w4n|@)+6rcz?p`;mKyES=KJFeR^X(hwQ~T@FJK?-u9qoB_wtGW&nvvcW;=v#pDVA`qxmy_dDMsG#|C_ zG>+~UDNRm46ZK!p2s~;DV~kN5k20NFj_#vIWYpodVbC)GhISxu3C*!UIRwUs!UMlCZwF`8lR5m)bN#~8g9aK)F=(FWlC7(JN4Qr}d9Nv1<8h+-P5 zz;@m8@W^N|K3WMA|B~4#N$dRi+vpmcpW=rVWYm%vv)6?qz^hCN+uUBNcIH zl6ZW(pax>}mxf*vH4zi*Qm-H&iT(06_Nyx(Y8Pac8rCzyh`<$-w}w$uIsFPSYqy!@ zqQJKdHSBdGATMho#HKS#4RdjNCh-4Ny=$~TNv^ZO2?f}J?6=t2T+)6Lh1Uxm%)#00 zxwNEJ3VSB|K35Z}rBgZ=EpxmQANi3fFlNW2O<7bw){4{LnjP632XBPwCLUHrD6mfI z_f!uw@DQ6{eMp&{_c3N!??-=6()gqNvvL}MEVl1ybU~x4!uk!PFSv0$D{oYNSSV?R z*L#ow3-P@+(*30@n12C$ujr?7mDz{fa9%bdn~IU*4T;-DRWAdHkKE*gW5Gf(ii|p@ zMf&=Uvv;}eACN914nlKuOJCIfJ+AQgL9gS8J#x1#hbBzZ_UzIIrGv9`3Bx3Pszpb& z6BYHBNi_^nnqdlbEfF-IPXyY0Sx(#yotqm@dtB)A<44V*IF#4$$^q5Gdz2jGnmxc?PXih6M=o$?d>f?ee}7K`Ys z_azj>&%1lpu3YPsLsi?~j3Q^#)`^xNjpramxkix4L0UgoNVNsiG`HkJXI7ET&$x1s zZ2hy9!pOR|eq05XSqGYx?woP*EtqBD3>|R_cF^(kA(v%M$XeNi4sjaFDvqOO=K8@V zv`9MeHCzh))r`1mi^lZuvxDh<78$;O=wK?g#wJGA2974i%I1!CZvUbPl$;$ufwq~) z-@k#mm5yWvietJ5d!Or~hHoRm9!Zu^=TwX-%3Cvvha70!%h2 zMr@+@%Ec&+U+i{|u4+#Y0W?xqTbYeq!Q7sx02{uqoZ`RC4K(#DHWc%hrqd(Kn zlUx<)5_yz2`djk&z`2=S7RqORkoTLLz29s|u0u_%E-F64=NTK*TQpc?1s7uE!838C zOmf*9%wN(9?>lzSxfN$LbO`mK#b33@ykoPp8O9q6k?cf&R81qtG^a#4lbBCAUkMck zZOXPX_US{AuHs!4eyHUMofK$gRrs?I^WMEO+j`SvFNr@JksoCgg9b77 zaf2*epm>G-mwe?BAbZ6g{$ezM3(Ed*^_g!)pG}?PA97CMe}&+G6PrR6EjMHply})G z8LkG!T!b}8g`Wx2t~HCWGW7iE%jS0J2%!`XOq>$M(x!E$WEF#a_OyE5*CXh9r_3}m zsAxRbpHzD5lynO?62AxMiqrk(*8Bbb>Fe9nn;6J}KI3@Cx=LnS9xI@;tfk44%Vo@# z%Cp{tB84@ITUPduXyv1D{}}~F|2jx_WGrtBK`w;^bfAbv5G5Hvh#~ZM z!w@{LI%lAHjX@Z=l8>?iTHZ+N9Ac>MzXB=B3`S-hQZ|@~jZFaaC&3XTl0E7eW{{NSZd%xB@-maMiQlSweSXx* z8%e1qkv6o%P{{mAdE7fgli|g$rn6z6$1#Xt*GyqA+l-sCo%sskEu$ zzw$=YL_Wtb!+YLKl4~cOsdhAR7~(N=Az#o93lEh7V<|DSopAp!Aya)SvOJ7Toz)W; zkl0ag2j5V4*>^t0eTWHHja8>?d>GI_crDu`KVo~1G6op71Dx&^Khgl@?Bfh7zg>N? z{qg%XO>USmiZs2L%I?s#aAZR#0>&vy^bYk753o8+ZM5L-b|(){t&Te99%gWyRHUG7 z5Q>?sSsp4zVIb2n1*lW@J~n!gz&%^ymppZ2- zKy5JAy;(%)Fc9l4GVQ}0naAHCx%Si;eu&jy8?ez9t_Tp(ChpV4DwCN*ioXfe!`upW zXBS_xd>Ik{Jj3jh5HK^x^OUX_&UnxEElnw#aL#Mmh<-_^g3(SQs1xgU)j1{N_lyp} zn63|sIpW?$qS2+%@-mOZJlOh>Y|E0p{FZNLYXDYtZP8s89ba*irS*)K?0mW%Zn_B+ z<84-vl#>6NfrRVfkXmf(%X3sQJVQFy$5;5RJ8Rn+&T}gUpxYNYBq#vn2r<+wr^K?7 zvsa+41>?9ibbl#4ndy7q2*LZn%Ah^bD+XVI;5P;xUw`8# zkkZ;Tcsi0kXXhFQuWu1w)lG4=bt6fLaQBc9XZ;8VWE@U!pMBplFL<@T@e0@CWYTs( zA)fx$`c`rcIMnDn5%$7mzv5LE76z45+7d&>DkLw!YEet|nOC?Tt%-qjEk#LqMC7vQ z*9}u;ko==THN|3KY$*~xjcUCE3e%8TWt4qj6W~p^Lr$|}R&WKId`X>rg{?{{I%k9i zJVMPuYzF}n)%yLV>iT%B@j-mnmdO7fVw>e}>PwcghSDcl_D<#xYtTxB=bk4z4iOPf z?hX`U`Kdy%kIkwZ1{w@4KNtqjUG3 z)w*M=c2o9&9*h>Ixgo~#?##WnLgQp5_o(iOr*R!(ozpMnz~!e4@hK&?|{oPJD)OQa!6t7poM#$gG7&L zCWSe1Y(@KO>vJe31U^N@GuX$qUFQU|*oR@&)7)%Q1Chg4l`Z-5+a@In^27-`%@hw3 zFMgST{Qy)J)=?euAf#`AQ;7+pqX8Qu#sEe1$}1@mEQlrsJ7wp&;Gz^@<%T~9RmYO0 z1>-Mv@5(B@qH36M%7p@R>sKXr^LoarR!gr;bauOh>n3H8-R#xW&hP}h=+p|E>0X4Y z-r~_ksPPz_OHsG9^iZm;6%?b%F`4e_JnS9oFqSw|_02QZVl71|hRI~xRJjPVCpI9? zxp^+aJv!xoWfBisx26_nmz(D~>8>;n7UZ)ZG$i&-wedg+^V=ZhQWoNa5p6K?{v?r` z@k4IN?|o|*LhdBvGXDrI&~lctYE!(J$&&0AqDk@y*<>#W*XQCHZNL<>Pwa|0;zhCa zo%RF_m)q)w^ae;D(;8}VB)t2(gRfzvl8RjXHZ?z#u`@D9R%>Vpv*Kn{Oln`i&}@sw zHb?~60uj8-EBi<(izwPoe3yFzL&yv@?t3|}^P#y`3%*xN*vMN0 zm+MS;JjA0!vK|R9v>ev2U^KMV!>7M^7ldId-Nb#;(`TRbwBSGV^Zx}y|EG*lq4K{b z<0$V<+JZvh6;S(R5M@oDUn z@q54g72AQ=aQ(L>3{R}}k?6OVEsvoC3E*c$v88Bu# zZA>IhSgj!-Z~5ijshSZd#NGW2Y3;-u-9|n9G10EKSkp6E!k{s5&V;2E(hLpI&4L%1hK4X2xSz738qW_))(+$8R+|WaVHPqQVQZ3Lc=D2rv}F6w1v{2*`po7J;6 zRDcGNt#)SAUi|U+B$dd$o~PV{a=SPf7f-? zLi~`Ia6htXwXPlf85#A*8T3nNzNH~TnL5cV&SKk;ncHZPd5~Q5`+4@znP6fdK?eQ))9YdL;_cvp{k-(!yDus~ z?0dd`lnF=go<0LkR2rDT@M7hjeqZ$0iAQ^y7G9(ki-E|Ja|~W&a^P8T-p|5co%gBP z(yjwC!;GgV=ZRD$wByzLHQ)27)gN|POZCn~T0yraqbH*`*1Wh-lwDvqK5Sj0I)9Kj_rByvqCOX>nS=i+WNG1ySXyLF4QBfk!Qf&LRb80FcUt7~7f#csb0 z#SJsco{@ZHtx_|ZQ!TV9DpVb)=qFSSv>aF4?gec#f{8Dt| zyi+gDLn~()@3YP=*Apr%ci|Us5fJm7Dd>@Us%i&&2X%xM<2UjIS7FIl5kftHp|nC_ z*%K<@MPzivPBu7+POIUH4v8|Ty)?`UglN6?1fC8s`Vg2}JNDP=4o)op)ITDTP|M>+ z#ap%F5DF8AFOGsivFxt6flLgF8pdQw7%!Ye&ynunHR=gXSM3Hg26DgU%KKJCJf6tM zy@Bczl33lmQIve+Fpacdmz5be{$&V_Ip5!~zWa@Jy)YM-TjdmFZaoE>veB9fyvT6X zd^u_jJO*J-Fh&^V^a?%y^BM=-&DsoQ>cv70TywbfTCu+Az)wD#e7|WHLzI?@kG`yQ zp~IrQpvkdXJ!4gNDWr|xhW^p&r*<651Yzq=RBj-hM1Jr{$%7>whelyY(#VJj&#Mv| zyn>%PX`61{y0*4r{0yHSuY;BL_>hIBB?guaWrN zbYupSxOC}))0*0`$SYx|guw-=UV$N(Hb_MjSqR&(^!TG(fHn*vAa=VoIKoi>fcMzhf*O33Q8hzGNy-pZHO_ z96&<0FI>SbJXG&PT1*A=k+}CX^>O{;R^B5cfa?-KKu89Rp+OzZyKbb1y1`zZ8 z1{!dS$^zlu4dINr@`L!g1FK<*^zy5%!zO)qNKDKhfz)RbabBt&168#T@%IXpL48E=XkpmyHM)qn%CL&zc+yZiM;sP6qfFnr`x zKb{$g1PNZrFMNmyo{)ShR}K6UBUcO3`K4*e!)6ITtx2hyap#+xu1zka%4`QAMqD_GBt;Eo#wJ*@W-RdiPL--Jo``fZu9DBOj^oiu?ujL-= zQ}y2a97)aWHXJ=6aG(axk&~H?IZRXMQb8IX=Ou&g)T?bX3`@=s`Cgk2S!naANfoK; z^IiYkkH-#5Xbkm;8)8n}TnaVdz-|jcpu9%ek*4;#Y0@uc-t8wOba^|XeWmi*@e026 z3wivrTZVP>`0}kwhmfzgk(ePoooouZyKfpBo?^66;*{c-AYv}sumqJC51}Inip1b<)oVruls_S?xo3mCgK8Vm{tlM z`U?Ca-K+*9C^F?B^Em^Y++ZsUp-8VxY6gD}8cqJU`Yz63a5>agtb;P)s_1LeQtcH$ zxmZj3^s9ZJ<9eX#vmfk-VB)mU7TmO#Sj+d(H>3FjJkf2! z=$G)+9Hwb^ePE`Y$&~&gM~6=U))a-TPRW(_uk?<|&UjXn zU>5$k@{q7uq*gAOMyb+ELCD)E6yo`+c(SwY^E2i4gq#7lg!FnFvhfplUD@!tNM$1< zEW4eL=EAG0&iP{>i%bh={P}a5|?}DTvnj zJU~(sN`Bywg%EdO?@DBTZ#p~z?`MyE!B|WHAL$oodN?uONBDA@Ldy+n+xXxK+m%j>-UqPZ?l0dteYdIensB+n zn0+0F_#}DVns1X(On)+OOk{gaUc64;yx&~p#C*Bk6$27RL_}(h9-7Qi7zgER*p9fC zVdl5#o0?V-Ey+x54<3aZ(OWLAI8r$8&!kA}hV0DY7cGvf^T z@MgcoU?dX`VZf7|$SSE}f{x6qpi=3oAP7?nX$cJ@o)UYS(CvK{YWuQC7pZbO`~^ax zq10Sd3O3z7HrFM@1Dp{F={?A%wXgK3&s7_EZft2AK^~GKT@^8oM%s7Lg9s5aGcx@o zy#N)h82)P*bz-JWdoc~Fjfr`! z20-3aL`IL(Po-pgJfkE|9kkM4<92iDxD6Q>{WAvSsWROZslBPvzpKywxGfw2iGme2 zmsmJ)Q(H_hj?%rUQ_DIc}=0 z#oH--U#5c;5r{FgBa+eXFot#&-t=o%bDdNq;lVj{f6!wga(&~z~6lZ0- zLjdw5Ge+W%9Wx8;8sCki=C?dUoIeHU1JM=VU3TF53Igj}dJvF&-GooPegt6N=9oZ* zhYN5KE75;{2|}Y^xsC8Hs6-$}-fTfYOgkk02(4p7_&PmF+|< zRmC1|2|L}3&?1@OCT4^ur;{Tm2QOz*F^ww+Gdf(3aVt>nQbTARZV(Cv#+j2UVkD(> z-l?v^%C)m=2Zo4ZgmBK~T+supN+7H3vY1&iPpDl$-fnu8_ZbJYAur4#IynPI>O7^t z2D6PVgTl-U&O1AW?pJh%Gfpd-okIDU`Bf&Zchm%g;4O+~c1mcwD%n?7PASj`7#vPI zndW`6RZc1wj($Za|DrPASEwyH8AE-47E|3a<#ru00PPcZa*4gc;d~a8SA1lp)q%|SUPyW)&AGdPW6F6rSvvf5y~L(t&!(w|mH#&O zuWo`j#oCbUEl?W%ErWQvLbV$9jr#C=K6?s=)%wUPZGDDiv{SEFSkaf&K(pjH446T| zm>+z*$}}h(H&EmsqE;-e;hQU4_SawfNHm7hyv*UFzw?i65O`f~>4}}^Tfv!`EirJk z2eL!Cad5Rox+OdKLZIio9wWX*XB!Fcl6YRy)j{QE-S2w2Z^0TvE4Az?=;iqv6IA!{ z!>5cm@`bkqenaSaaQ-fC@I~xbS%TmxQD=SuVsW)m(1r}h@Ccp2v%@M#F+&Im+R?L- zWAP(&uiPH5@P%?)ezP3}vruoe%J{}`_NLf$vO-6QJcdzMkHv@{yM4l869NRXN*-01SSP=i#H!ejS-GIEC6>Wf!bnfL~vtcIiIE<|2Eb zNq$5NWe}Ej#S+lj{0OaNS3>r^%qCnfY!);_l#BlBRQ4xB=kEUJ-0bJ)WS0IP7^Q!c zC;v@#wwd0t>|;QIyu9GB$(npKI!841GQ#T_3m^!^=5VbqS2qeRs3cLiTBD9XUBh6u|MiZw=o65{nU%p--U5Gs~Bzd@BKH_FcDGf!m zUiB2sJi$Doi)Izqf`A_Mg3Lh!$R!V^-ZJAsO`eX~i8-qEIgcFOJB;yKvx9lCKh9=t zUaW7b#Mov`m;Sh54BzUE>n_5V*>Mua5dOnG|IMo`u92Ag#@h;sQB@N>2r`{QcA5kn z>PwD|cJhiq^EmZw*>hW&op;EGjvxE<^Iyntw)3lCo=-CT&;Jh@&h$4i{eLR&o==r` zAORhPBtAG>cixt;HZDF+!E9vkSQo-b{c( zBO6ByiE%dslbC;Q0Imeil=4dRc%%lqd8OID+C+&EwS7IJBS_)y+|(vZiAdW_I3@hQ zraKfU-39>k9-h=LW-`a{WJ;)A4Gyr)GE8;K{8RIR*D1}~T zYuQ&f1tqz`;Wx$K;fNNprwWC_v3Kqm;jsk+C7eg-0qM!Ab1BArKXYizMLJt;L^j%6 z1>rg;mZeT`G?s_~`hL&MnsdD6#o%$kJ4Q5orc zx+}AVmbZxN;vVK%Z4odz#lg?dJjNfWafB@$_F@}fqQ#gmf&ZcCWS^;X7$kfi)n~=r zbXm8+)#!iYb78ZY!z@Pr&70-D42jvldx#AyM}&{&F>?d^??Sh_+1Q8hPjE_z_0K}L zf2(-^gFXMdL{h)C+N&iKot@QVCZyg$}1PoGa4HsbadL(UbbGy>SHmlVDZzuwpt%D zZ*uN7U&gg!@WABhGeN0|L_2!d7Z9L;LcDNSsd$WTB1Ug;x$gn?{Y9~e=NniN=Wuse zwN+QF#HJw#B~!gCIEym%g?uP6IXwyV@>x=bZ?-^o7iL(89r&pvKK@?>yLPWY^R~k0d6m z&MV`YjS})u^Q865M3z@;O)}h!q(XXd^dQwYPVEk{uoy~;`c7^d8`z#+WpFG1G&DB2 zJ!!eJG+Uot+ghMc50O4c`ykDDI?Ac#G^8OTfuo7_gv>Sy`hu-o1?@1tnB zXOAi|Z)X9XeSOk~eFhMo1p28({+HKADYvYAMH;p^ks*C>HqRhmP%Gd>1g~5`pC(sr zu+R)@!Pqj1^dlaEf7Q;ir(r1CJH#LYL|(t7U4wFG>Rt>W9N0fi;@pOTS~f!~%igxE z`@q|*pH+1y1V=tx5A1f?VN>pgey0;6MKZ~mJP&3Tej6iU?CEfj!9@y zQWeIVL@vonHJ_N#&0km@MQA+zh5_biI1CXf+@_=BQ+xQmlia$Zw(L~!>+3YDb@MT8 z4_(6QIwM9{$a55nLqj1=V|EjQae!0pXi7!~+0?a(LGobtFvc+p#t+QN)rLuaq%3UF zZf82=DN`UFn{vpdB}R`(JPvZIuVL3R;#4T}{&H;!B~A?6py6|@f5IRuV}ph{*1b^) z#)~~8?G`5^^H~dv)jqozv8i^8 zk~bWF)3P1mj5Ly6V<>)-i`4)-z=OE+(~DO{gW>d65~X?9D1LW$4~mSF1*hhh8HmQ4ge*73^_=_1zwHdwMyAJQX*}@-N9bP;8YDe$WpRu@5)qzG7LfJiiuu zfW}nxP%PYAca(`I`UD647_Pq&@C*?p(AEEZJBra4o)yC^S4OksFx=nD9}fzxm1_>Cbj4lqY! zFUP8+N&2x6C~0+1EvfpR#7+;_-Q@ZXZ2ei{NmH`jth)Iss~_X!$X-u#wo6i^MZSoS zQ?sk0oB7pH!K3~*<3;cagt=|Al{sUqS#ynRp~{pO=X?gtE3txadukTgZC+9b_{abg zaaZZrKDmL@sG%R_-rb||Ry%m@4={X9h?-LVF0Sd+I}*XOrJ^{#z0PPJw)3@+Lv*nCkbQA*zr5k))PN^Ve3cm=R9j ztQFG@GaDwv{YEyd>Hf7_b@5G#_PrSI)nFpg7k$5P$bESn@$e?~bjLX111|uV22QRy zJkWIZzL)Yo^HlGEz1ra~Ox(qWSV1M*YIvrX?JQ?rWZh4WI~{@t30N9kmKEZWNny2A zjH={A9+u+?c0{b#3;l6*`zHxlXqV*ydV#wrOBBgC$!i0G5K@S>D6;%CJ01KAqfcYx zN`X(hu2k(PimbpPFbc{XrpSFG^;XaD@?*pbN8G{VbA<{IaI+Sw^5apPeWg)Y(c15Y zruBoir3&>ANaga(VO(+(>7kr@6KvC9Y^+v8qJ0VVXjNzwp{U@{T@YvL0(@0_n{(zj zRP(#eqkHv~UtW+LB{wbA3PVanw<~deT!0j{;tgSDw-F!y8oo#Qg}Qh^^2-uJrbL50 z=})N!`oezs3gmbPnF4>PRNn^6bNgao!9JBbF7dfecq0W5yu0_0R!L<{(y7N zOUnht>&CglEe?6*0|Pr(DkXD`R21_}ioco=VWBph0ekRNULC?cg?q$oUvrT2v>E)f z0o7z)rmCY&5e~h_q;XXPBljw0$OVIB^5y2w?OFep+c898hzjWXR|f!IFR4cdgwD{% z_lMeH9Gj<;GTz~H(Xiq+&oS1go!@KK?L}&dlNduPfpp0?Y5nfq>2np><_j{fFR`zW zp|7nI`}dJM@ONHnqi=ME_Om`Y^2tatL!G&RgB}H$Q!5a!cllq@iJ)-bGSj>X=*XI*6p!?tHpPRES!a#0Zd*k! zTiIf~0D8Cf&;E6G;NQH~hea6W(8={vATtC$M zotk={8+iBcofH>96^d!#g3a3_@*?La`({eve`OSZv6C*cq<0xpu5?%dUh({R^vU6# zc*k!QyHt9ABMSCvYUB%;>9q@!rZBEgqJK!>5>`g9|Dn^D*OrLGZnpHOm@YadlrZbg zS*j+#n>6o^%Bm{^-*tfW(-S-8|v^O)JxW5hWIX!JkN_c{a7T+ zk4@C@>R@gcj`-RcHS!iNCV10gBc`x^W@zzY1^Boz=C7~wAZl>n_ptCkC%^jw>KW?~ zk2tLW>PZ7OT`9q@Mha5u-jxMPvYRHTLZkq_$Ir@`z57|KB6b`Ua|p`F!;UFMuPhvS z$0)w12(op`L&c@=Cv&;1pulIjABVpp%jr0T3-cqgt*0O;6sl5qtD}fR8}9g`z;I7VVpD(``dSvBwsQX6 z=lS;5`{U_}+Mi?*!&GX0Aya39Lfur>$)OM^K#_<9Ew`NiX z;GUnoRPs3owaA=H>xTaoBY6^!DsVJYJN z(##Au-ljCqHX=f3b7MNnSN!#CLrMU{$86R%P_?Y;Z90{@q@_ML$Xbu&JV0g)W zAayQMHF7A*5lVL}zxk&leV6wwUm$eMxA*Y0ZPO+b9$k z{pc86ciz6migOm>0orOzABM7Bf%9oBqb*U~UucMEcO`AqQ%WWa9ka=pGbyp#8VlEo zvg;p<)P=Psko30b4-R`5XacM~w%8uaEF2=`wO((RwGhyZgox%}4^I)1 zi0p}8Mu#`wpRXqKbqiGrZzTp8qA>Hwh@NLbE}WrLN+Su9j}|86UwP}I71MeMa*6iB z3~_{laEpot>~rp2!Vy4CPe8K~WcT2pzgP+?bIOq7p(B*He#`JlCxD@l@ymT;% zT>`BW4hK*Mr)B0X@H~9-`{rX>TtlChgeeju@NjnM;^^^}MjNNO_8mNvj%9zwG{DEk z#Nh>)zx_pOKBT9K$0v z*%a47u}DE_C`BPfhNeadzBChVnbymA%QV6|YI3 z@sZcpySK0YJa~wBYkhNEa1-X52iXU$Z5tLG=Ut}f&9rBYI_SW{_B>sS*2&y1voNL* z_q->8FNk%;J-}Um%!+io zPRyQ}@(pViNn%Kul_3f<6cUA6^v%J#y5`A4Fn?E47Qv}U3_^e>_LmBJg(}S6X~hW$ zm%#T4zNF$+EGLes-?~siy=f7J@}#ZrN9O|gZbn^)CJCw8H3v(jK3NR}UHV7JbIgU$ z=K*ji9B7?LmC({{4Ln!>M7L;Pow3RA--m+#t8xu7}A6G zC}e^c;iSShlM*>s2byS!H{JD3?J5qlJyBGms6Y3~>uX`^gL_7E_b)`Yq(5-_q=OR51X#q!b(VX=Z1D*2!+M)uTD2DbX!>1??? z-`i3Li{rS~;}-tW9RPNZIe2(7C(C(u-FwG*mh<;>Z|@FJdhjH}nM1)F*cQ!(njU)Y z-V4|&^_g0Q&9A1-P0UfG_Rth>=(Tmm`_{cT7*c7-E8Hk3#)sUIOaQAN6NVDHG6P!) z!NB*{Y~XSHDZD5)-MC*-Iu~syY7UFA37Ap)_)LIkQ_?&;Y&peekN)G*HFw$3RZO9V zmkw~$%t!`0+I)zC>%I{5W3H&Ex~JAm}4HudriUWbayzAqeZxY&HC zT_|>q5y}IT1&QG_6<27vNA{jtHRfy}$oxZr{;(@718lQ{ibGOCQoVV%QOOW)g1721 zpRhm8$392kUYd)i#N1DYVx#YavIQr)qv)Z;6wXDkX--+xFxr#O@@U7-Dmdms+anN) z(R=X1?#k$3j$Xze#>4fJy3g}}=ug}!-_Ks0;SMA!B@Bo>8dT-jT})VncgB5i_f`@T zS2K*lYw6ALY$eCB_>v=$+C~|i#!?z=^;j;%8qxGJyJ%=u6>k`YXA5>RZ)nOm)G1L+ zw0#Qq18G{q73rMKvk2E^{98RazWfWsh<8ZA7LG^JlTZ}gRZbSfL*?)1jo5LL)o zovoBTPvW81no$F>BTltkCpQOT{Z;Gfo^>tHJ2APck%DXzBUqrN%_OBWsGLzHSnV!Q zQp=S=1C~XVJ9(}XqstI zli}<^$*_whU7`d_jo!1$xtLgRNxBzGO0Md^!V2fTbwfjJf1j;`QENsUMz4B!vGr4A zRgcps3}%wV)7oyB`M0q&o(1OUp5FUT)Eh?WJa%cX_1_n>}Wzz$&W$23;y zXqCs>qbKUUs*mDelK(c68@8JzF0{uDYNLSf&u~(u`6x{qt0&9Mv=GcVF%{hlK zIpg$#gr9*>cO_1M)pusk=0$fF#`x@VLUvp^wTAh}>PYlDM^l6evqE~EL3jJt0eg57 z0E`ekNwg8n4st#Kb1W$hkU>HwQgJ@Y#xXCUsryJ$I>r@Z2Pm1Ga&gNKAR3n70#oQ=0RIL@TSJeb}dw&>~QP1u6}hQngxUwdMze8>mNz4$NuMoDZ!&wD_{f z8f{Ahu{-qpW;mvcn(erl;fRaMRz-6~9dK~X>|ea~T)$mS`Cog>DrFiZ_>r(bZ##cD ze>4HS^Zb8Dlzjsof(~Rh4&5?=V-*c8Iu=OESu$Qx;Ne^`i6t$R?N7HQAr>3BVv0F6 zxLztuco2dFg${2Ocbz=PVYY>qPohNAP*gBANaL%8srFTUT59&#Lrh=Y#)58XY1RLJ z1??8f1xh8XWUZ_c2gm-nCdtg^)7saV`7~W%pzYypZY}OH`ea~l?jbc3<^;5}L?1|6 zV0nDHqM>}f9V|$?J#)3vszAu{sWWwG6vS@@-}Mq5ej8><HfAL7p`MO`Un%gJ@S{x6(G`p>vz4bK5-lUG>1jM3 zgY&L1p z$X53u+Jj;?iCMEH#*#nKMh6bOSWGP}oSU0rlCc{ESM}j?W?a%jf5VfWAc_V48aE`z zEr0aUA#e|AOO@axvp99jZ-s^=n~|&&_@+J3meBlVN%;1IMvfPW(c~(v0_${Tw7hsq zigXP{l46>rtT8bw6>UG~t=c0}b)>N*ezw`mW{Zs&i_T5y1&K*;LuJtwMSXb%b!u#t zP)81l4%vclJ9cmQTTFr;>BVK}3+)*_ZO z7$4j!65E(*Z*2taai%Is-uUTIGk+QAR+ zH)v2vC7koHaUda`m;s1P?MyjF9uCVxH*+Gmnfh+Sd;*$#m-S-}qRA1z!2;-V{e zYdP_}oezG)cPWqD>z*%dq_V7QPdN%R=}Qp)I_LGMvjM$Msxcc1j}~*8N`|o51I-j=xhKW!>CD&g8uE z(apYPhr#$#h~*Y zK!U_0;K)9bIV-v-zT%!oFP@8klzAYQ+Zv0H{%yAD1vsLz=6+8axNNrenqszBC)WH1 z)rogZZWQf!qjF!wxOe79?Mj%vlBaGS!j2icES51>(E`uppIOfIo~Y|_1^+yYL>PQO z2`3^uCXO(3U&9(tt?Rg3Xz*f+nl``+Y|TJXRDWzuKai4 z-ptJFvn{-f?dm=DnLqYmTxR|{s9kcDLH_hmA0VE}$*(u$dPE-e4?{_<9oZ19f57}x z$j5flF=pBi2l<`ke8!_g{U!C~M|v}IDb3^?CuJtk>YdauMQT5y(n|h(0`9jOtunK+ zz{K=zb(e9&bdFh-o69((c8G4Ho|;X})y6S*LM4`b@2iPK`mmrW!Ue#I2nc}}2}`{q zTNAl&TjIWzvJL64Jo2%`u>xpTc z<&aaD*4?5)P_XD|T!5pTs8Zgtqhum%4DR@dy`A!MSx9~A{_F~(G8v1xTiFDtE0~v7 zo7+#}F?!^Y%=2Ks5DYiP znH6T_|79r+Qe#mp*HeX)Q<$N{%hH&U5cxiF}~3FV(+4h;7J)>i8@ zV+c9~s`gtN{s0z!rRG`9bCIKLLYtF3Mve0-9zW|SbhjC9*wV(a)ppXZ69H9O0h;93 zxZ6e(^vP}uj%SG}X5Lc~M0c}ZEZt$bM9k}{as$>SK^m!*3*xI{r5Jbnxgw2Ag0{fx zrckc>eTpZViLs?9l;11S46${cz9x0-u`KS~_2M{%uO%!x^__4GG1R5M4i%poaw zpUa1TIup(@M7TSL=wI`L<|V)9D-ElRdG*dWP#wI-OqdslEu0Xgj@zYt?QM^8ia!S* zigWy*4~QvLc`i&S11cEuib8xYX@^Od+$hzU^5Gzv57P|cz%gzhkH8U4;(r>1BMkz0 z2UL@K-S(WK?{JZZm&5n8$ohat!trMv+NO_g3_=GXns7=TIp}@kQ1>Z?yJ*nT>c%sC zwhr%nhu{J?XvwdV7dD|8y%+{h`(cY9n4|F=FnMu}U($_OVi#{n#NmsC<1=ZlaL9`~ z8-lyCgSr9!U|Hj;G-XwqLaPke&kZV1O(M0VMXQP@kB9HzPCfzETOu{=%-Z(-DQzj8 zcE&RHCnvZ*j)3u#LEYwa8L<({>ZtVT#$@W)zRl~jWl=lwo7*s^+}}Bm(dMWbQWbsV zFTD~meB)vY)m=B`EuHHazh9jd!8L51^mie1&-jDP-^gz=CT2@-iL4)nS$a5FACdf5 zxRsG#tX9Cw&cIR16pHF9*z$(G13VB+ZbEgtmfJmc(j%T}(d+KG!35R6*jcmReADxN zklOR1o|GvYX80rUgPjumNeszunIVd|?@8RA(eX#>ykFM`{Z|+DwRV^T`SD`9}NPddhqtS%X6N;&1s=WNk^2Vo!h<;9sEdor`g*vz;F}rFsLfx+EyKQ+1PLmdKy!@06#z zZ&W|-`EnPNUS^B?=DE<1PF;weltSbo`xkZ3tej}+Qbvd82TBSxGP8-hgjA}4Zz7P) zGK-A5y;W~sByW3)pdJ*$l6deGy;SZ@T0HNpy>C=4e#m>YZx&1_pu^|87EolNIO7W% z6r|UIQ=9tHpuoUKiIUoJftmpG1m!n1rXvsQC_0;uv7H2{R+>35^~)c%g~s@i%Iee> z>QKq4AflZI9QJUf7e-_j(OjNC*0ysie1wU;=lji-N90Ke%prUsS`obi9Nx49 zd$L~RSJN{WJa1DMQy;JQ2jl^G0JGDdZDpNJUS!shmRruSlQ}IpU37O~%JQ0c?o3^~ zG0=81GS!&vi}O_hjM;04*$ue0LyiRgHoRFDkK7vb~wL+R`oRNdJQ-1|c$x@ak& zg0Rri9>TEC#HL2Y?r&{7`ROM7bkk;kFN-@wl29OiqFzM37Yl_lI@@RcD2!ERXVvYpf4_TA1`rf4OsNP~3RuGU$SdW1O4e za$h6=gms=8a}KQ?W7Ej;#J1ZWm9(%JQbdKb_)@Z@W{1wiR3T2A!OVAiL->oHbR;Z(s)-hom-!TQ0EZq;*;lg9~~UYU+0TNcK5^+`-J1 z3$fOSOJJ4iDj~;?sO5`xrYbf`OhQL500jJ*3ea48tT7-I{#N&li`pgRUj&2+AGypYC4J z`Dxa(o0VnUtkFLVS1LU0yqa-57bnd(h(l-GXC5ycR2)DLmxw}=XQ@ND6OXTmLN!e< z@s^nQ99enFy1uOtxxEj+y+^oxfXGJ>6&EZ{su{^8nvSFz;Wpf}w*g+!TIiLNETj_Z zqXb<`tSMD|p7bU&>PL8%dW}n@U}@MB)6@Ic`8m(fwj$%}Y}Mw!*y#KA^2?@SO^2_OuRqSAA}1@GdWFseI9!cLy#ww`}}bSeJ$T*{BJyW;b7AxiN! zc~6E=DR~|kVW?hf04ML6n5PV*3OTm=ID_yW=>rjF9K+4VY7Zh(CF$V@N)Ig1Ai} zR+IRZ7b0^}U;JZWYlHYxk@U(Stu4%V9|%4|oP#WPH`yc9*$9ruk2o~yC3kS@-@M;RE;iJFzfyJN z|DU^C=D%G!{68WTI538s(KwM1<%&t)HBb;xhKM88c`5}a*sU938S4KbL&5PSLqR3- z(7(TqZ@S(Tj1)d!X|p`@@z13LW0z4|NwC`h>8S3LN&h^M`d%VF3a%!N z=S(%q_Wov{ep(b!I)8lx0jFUg<3shG>y?KN?bq6%YG4^H6QdR<;_K|TT+h2J=BSXi%971482D#$cCS$G8?acO>AO z|E7S(Mm7a%>hPnmI)XM-#J0u+DKa3tawM2Z2Z<~@>^_Sa%@qEud#kiY}Wp-Nf(`91IO;G zFA7BY_Ko&GulfG3fux$Hv!#o@lZvV7-$YoA+M7DIC`u0u6lMGZG%y?qZMlU9sJBe=zcY9<)i$xDq;AzD92lqmYrVJNqI@1ZY|U~?5YiD{;3ik&#H-mZ1<3P7fg z6hpAob9SLy#vZl8TjHAp4COg5a|{z);}!|wT`;1Q81og{Hemap!XspqU=}cJPuk_V zy4RemC(&aZcOAVVA;{yiGnPUBph9oxwHDz&_UDqHT0ZeWT{2Z+El{7e%~XB26Ld_M(m{i0$X*r)Ty+XGxyqNjZrgPf3!Sf z7M_|;j28(yHGmi*i;@~W%D?mt?HU> zcDKPK@3z%W+?oj8S;nREXP{X|kkbgii>y%1ct1lXMn#>83DI$r{-lXH6v(S6VFgjX zPQflK6ewMFrEZ`}Kp;Y;Qp~MuO;z;@QIzQ+bNSJG4!(Lql;;X3#D=q15Xo3?Q6Wgy zl&DQxs*<2kO;sY>F_f$a7K3VweCm47F~)7Gw+p*6TGssNxTwOO1K7$mDx}*k{>d@7 z*@ed&BT%EnkZ{s_MW1!1eoEEUMs|&c+_++`QxIs1Vin2UyR7#8 z85vqleR$8kVXD&V{C8S$h!Xpm@9_(e6KxeeW@QSqV1FI9r0pQ(c$=Mta{EJ&8Tgs8 zre>bNI!Z^*y|vQT82DMN3}6;zBh;kvdRZ*pC}b_vcHnng0_=jWC$L|L0P=pgHEp7y zm{r5zV5@g#DKjT?LC%~q7TpF{mtcI&pYz=?NIZ+0Kd^$>LjKHPx9{M+$GeJgZlis5 zFZh2#t}roPLpJ<`)Ny^I?Bj4sy(@AgG;rh)(IBdeu0+9^e zH9Pm2Rnx_?VazOUdm+xUMG;9!b^=md=OKA%jNIn=HteOT6boPWjan5GHPGuAX-se; z56|-Dr7Si@IY1UxhoKgYTZ0S(HOOhis_6xA`qA0jVqR#v*6MVtspX4YoiqpT*s6rN z+7&Kcf^!8R?UCULwDE>t1RwSje-7e{Pv+Y?A;)rMeH(k_TLhX7p@AAqj8Eq((uRRDm1_6=aDr`hZj!&5A;Pc3oki8cyms8G;q6 zoKhgCp97DbZHdciEsl+-AXK=AXZqEa&^-k04c~cT)L>G%M71Q>7>of1pDBfKz%SoVrG6 z)9QB9YE^n@czq2Nsz@WC0~`$dHt0Q9_AWi%KcZTc(7oX|(ir+;_LWYA7Yh|dxC}- z7$M?#vV!69`8tG!W(xLry2;`^N1q_anE4KoAfXsUHR=|zg9Ie{R?z#79ZGBWI`qQG zbAyjej-!4xx`*l3(lt2;x-M{&GU*3R)d?N1GQ_&;b7nx84tM`@U$OYCB)j*1q9S(D zMfs-9u9MLzb$%_|Y%h5HlMw=eJ8Q@mdN;0omK5>sC;Q=lB>t zs||$g^@Qw%0C(8vr4h20l>u=ftuiq8$4gc7caDQ5#%zSpFa(}X2{gEeJ z!HxE(?*j4vH>870F;;BuYlLe5YN7wex8;BKjh1%i|1?S#|Jql4$+6ly{Y_nuQQG+j z#_@5i#i<>O24e)4-1?DE8yt13cmSiQkL6Qu`6ZREGebI>yRIGnnHmw?2jU;#pX}%s zMViQRv}a~+XV!b2mGS<5atGaq=M}83wb8~Aoy4Miu!dC-H3+~N9rVv#IP)Wm+%H;M zfXvGJE~3GQR7NxtEO#J(K%BAmomA)-e?191Q;+X6o}o3{Q|=GaWTgtIoPM4*cF@M) zZ38jn99-S+|Qk`!NYj?P{p~B8?#bC%5s)=&(wm2y;AUu(Bqj^v2;Kuh- z=z(9FeNE(p3uz0I1iqms&Q&0aI@H(|LhaOY%M;Uu+n*AL?p02>`;3NfvE3nS1S-&$ zVFSerVje-qL~OS#1H>n^`NP^AM=djogIm`p`lJPnW5@|H!HP!N;|0vYuL71|qGvp* z$FK6Pm1*8JelzkiMEvD4h0{$mKh(#~cSok|IUR(b$Zw%j*Z59OVMa;$e!V->JiE`# zonGe}K90V1~YSZijzLnF-w)TGY5pqxeuYF*qzVQ|G z*WeBN1l|W@RlNy424+g3vQp4e zQQ63ERiLq|N;(t}8-&ftpxDszZn(78e_LwtxoxRk4mH)Bd3R1*<-!29epr^Nz56?fA>MUIxan% z#nP>f6)XU7G>3EEoFnMNYN-JqTQ2Vnr1SS;#do2gbERxAJ{)9-KE6%|emI@Q1(}y_U$;WUW5J z^U}1?o=r-s08xf5#Y`_ZJ+ClrR{V#8Rb?idR-*HQnU;BDWvR(V)5tJDRtZN5n5#jx z0U#y_p9A_D4$ zT(sBIf-1~-F@l#vc2yFQ>naZph7nZ&@rL>d0<5UH%!O2rE8Shm9!jEQ<*H?Hon>lu zio7P1X->mle!3OWWx2V4%JG~@)K&b3#cCZmR*oMU`@=)q;Il)C1-5=w!fI+JZ30ZQ zT(y8XqeH9AW5Au(JM|^ZQDl@G!{kIQ1@fUc!6(3&GZ1lzBlIBBSd2i8zi2PGujlZ5 zxz4E^FUb&>{I<3aywMYe`~_03B6$(mmRc9ZWsbs{U*9Yxj`j2m zTz9)1ac0kldd4Wa6~HEKdNzCm`7=+ zqm+fc6yc4gVi`=DUx6Jibf)T-;v^b4fAv|12Xq)TwWxaCBomz!gYw%COdRjVy#r)x1yTXuR$v}Xs&283|nr$ zSapwOm*6)4KsN@)&#|sTEY5(x*-gB!BWF8N5w4_>{dsHx_D*;*NMdeU6hypoUR zpjF9Pd`rt!BT00%-T2rlim0vXfa|J3gc5SqIuO zBTD^+#qGg1)dgDB$gK#hP{;$yfBYh=&;>f2p~^lLXFdt=mOdq`(p`4}dm9lR)P?>k zw!}KhMYYggc?Sk$I0(NxUbB-8Yt*KQ$DiXBHj=N4bGw7Lt;m4uRA$(ks|tbGuR1InbMk6Z`;w za`M9$=M~wR^@aaV-QP~4sEi?;+1|}VO?mv(cd#nBt6c7nr$XtsFK1N;;bJO zsX4LVa>LxQfYb1tdtd?v^Yv)VJ;A>geWPCXXUT`w#fCtyR-3QMloH4E_kf5mO&(c$ zpfZi}YbiT^9COK#$1;3UATkYcj7%MH1S7xXHW`}fp1h4P&2Hzj{;s}cSA^I?5(!l& z7%+6@;q=06pFIitR`8quugUi7_mcVeGXB7X`}U3IKPTINmgfI<=2k{lNBOWD6BZHy zLqzXqM;ahr*1|Wir9q7$k`ScQXvwM{avd0vWLx_z`h?7>Fg7-SwdQ^=;(EVYmrac) zfL$~M@V3X@^Ima!zn|>L`va#3*EkF}09;IobLdenbv9_TDYi=FN~dG?x~d%P&+DLz zjaQjM53!ePSGpnIZ}WpdAh@u?!u^96>X3!45ouZRn2_!CUNZqK4=j zxo8O%Ceoiu>GEb@jhm*l-MSm}A_S)AJXdi-g1*I$QRwmCcGQ~^z`sygZ(Sv@87h$vz2;#wmYceHAuoc@V$YjO^uX zv^%N{@sg)KeSP*me8=ag|2Y0jJ(60wsjAXrh8O^j0oE zo$uJ~_ciIu^yRYQ!_^nbtUhlVBc~v7wwHEVo5PHS4i*PWdo=Z_Mq4DmFFwiWvCU{^ zo!xPDBR|sfx%CR2R$CtoC2T6(4xz95?GyZ2R^&CuXlMNH;(ncKtow~G&11sn2;Z)V z)gKTq{}ecEpWNqCM5w4s>33AwizP2z20sg!)PP2S$5S>Bov9RA@j2}RQ6*~_5 zQ@g>4Y(!$s;Kbmu4{ULuy9nK~a5sjfyu`bM7$zu*7Lev4hYT}}(9$lsiXw$DZ4&q_ z<93>^2`2P=OrOa;MLt2&`{)eCP%8}8!D;BlOIWl8tUwLHKiA07I2eQ;B}a;i)Aq5p zRr&h77b2E`Mn=eA2Uw%UKuKRvzv(=qu_q%$NFlHcQ;^K3*ZD{QAg>iT`=3d~7XgFP zZLZ3x-9yy+CMsfTzYj-O#5I%c3FgQtSPzBp!bJk>mA4DQ1@P}Q z%p(PcsQs?>ob|~4h~JF%Ay9IZ9_O97?q>qYUuB-j7dFF-b`0q#rsMC3Z`I|wQq zj6wn#UI0=AbOaG@j`6!lZ1fMCK?fArJ_X3mAZod?Mepm`d$9ce#Clg8)6){X>NAsv zi4ea#bIc~K2kb3i6B=4H%jL@F1aSW{WBht~a)SKreT;zw?yC6WF*$Ye*Lt#X zMQFF3mUcx(m(}KEuc+O$O?k&2h_Qt39%D+2z`bF5`{@Ze&>B`MWJ6YS=3a8__`gz;8U|M&+m^N}kJOC1_1qI?g_AWE@AbKbV)q zjWeRai=eF(umxCy_+C&v_Nszm9%1p!g@0ncPLh&z3Jj}k05sKVaNIGtiu}E++~(~? zf4YvMh*CfnY0lXjp3*X>AS6&VLm`wdGRx1>3BUO}DN2Ix5UZ(n3N`iRz&Xbd;^%x8 zQA@{`V7JdZu~en0HcX;GTh4H{HS2=$o}%bpnOeM&o8ucncg*RAa3D34SC%?%qJ@-e zv&_wzg1h`2v6YP$oW-ncGEUG9xVH4!oR|IK^GoW+?1tM6NM9$3Icg-T<1FXVl`*Q| zTj=l|Ybqcd-7gyQcykkwZ%1BK@+G0oa(PTKBJ$vPuH85o_Xcfu5WBWQnfY6q-)xB1 zFfGRvQXe%elgYStJTxOSQ(uu*cl!8uBL{{RQ%8T^m5ph;>Jn(`6t47vvsdl``1H8& zCquHZ4^A*o@OydjJ)&e(vHN@{ogXM^E{Dh|_4(;D6db<9dN(I}lQY0&E=$+ zUfRk*>1%?c*jBQ)Ox|hKQ34gp6LG!}9YFY{?M0Tz^NN1spU|lH1Q?dj1d~Vxeq?Du^mZV5BYf zMd#*t&xPcTR}V|)Eu;2A5m9lIR>-17dN#&!_Rz@IM2Nvx3D1)%Cmf(v2{@HwSHvUn z47QLn3a`!^8$<2=_Scqg{yeT1=!;H8{550$4FlzWBK2R$jEueEmv`*n1Z<9q?msMY zvSo!Ax$Lwi#D%dJno%S!kU;7LB@`+k#~*;CNa8)(+R_XTbLZE=ZtDbqRRe|)cEt&w zvcDrjl_Ao*dvC92JOVcO{e9nn@?&&`w8xjEU-cNm2XfP_3KIRQd@bKlI869{2=V zeT5H2h2Xk+Pq~#jp9d*;VeS#O)Y5Kv)NelDEL?4A`Wh8P)<<}xq9_a^6lCv8 zs#!m}P_?L@+-S>aLm!wO2WDt!kuCyd@FhuG4Co;dvXV@RFj09@G6bxdaEG+u?~P;S zKE5662tZi}Hzr==SCODn#e@D_&wNDVW~(*mj;3ORbT?~p0oXyQ+v*IMI!k8-|2ST! z=WsS31X^oU7-jQEwz2o^=nNL}N@IBIyl1!^7Pnce#D^d{hsKT6BSDxJ(s&XkMUad2 zR*K8jnB7OlmFV$}pv@g2`efEvf+ZIRi$$*=vrdw)TP5tw*^(B2LwHm@kV&jyw%y4-!(TpY zM-LSlqc{V3zT$l5-uXS#&i`_0LxPsAN*VKOc^1B95{;?+$dONhXjC^ zaS(2=+Qkr0Ak+1WI9p5C&PG-0DCTh#ZXoCUlSH2Y;Jo36QJPMG%BGx2uuLrBn;zg$ z>a{lASZFl!XPg77L3Gvm$ag@4tI2W?Hj%9GCH#Ae{8oHCZo9w?o-=TPru7-2t#Itr z=@c0(sU;DL(;&Q-kQMmA`W{PVxV(d+=+t+bSdH6a$jnw^8l5XTAE~ht`S|StmfG?@ z&TT(WmAlbqB96q@35*V zLgRgKK5`vC&=nszU8L#Ao!eX*h`oq(nr&Rokvj%A7ruiD_gElsG51&`I#w?lU=ejA#7QIb%&>EL(69c}S+Zi05uW z6b%%H)hzsM)ga3zmzBDE9`hSiscUHO!PUZqCvp^Cxv%Vr4ECzZ$!>60Ab*YOv_FHJ z$7=E=7a_cNj01M=t+o93#><2{;Co?AKO+>l_qpA?7vXihJ89XL`XHLly$V;>*U_a`%!gI5!Wv84?JJtqDT1n^d?Q89Vp$6 z5t`b0b$r*$vM&cXe3X8rkFeHI%^aAw%}}I}RZzFjY_q5*8gg@&^e}hTE-a^uLP?q- z#QSUd{xJHX?AsE@?Qc1*h{n4!>5+Sak>-RB<3$@G3iy?RyqGKf4!Wd}HL`FW-e8X20`ljEeQK4W8_OVTZ%lS8 z&+$+3qv(reG=sO$&o>pC5B$GYD43wWhqkW@)$&!LNdHsBiy1n*$eP+3{cTAKNz(r6 zA-|7)mZ583n0v8)+G^NC5nwC?YZ#b&UyYeU@SQ3RbOgpWfx30wf(F{aDB~qq!cJx=yEd3HbZmMyR(Mr|# zB@Scj?LM0X;!%t>g6R^S02?mXDlJ0O@~wQv=wBGa8t7yWTUAdgsW!@I05nkW55e3= z*4IyY>_J(eoaPgi4n$l`8OyeiYWD`c&4!88>U*NN=1TtQ@$AgirkNCM<-9EehgF-4 z+9kK*Sk7Wqhuk`v9lT7oP3$R@AnKp#zBR@`?yBd!bfU8Py{r-RrTOQzYD|~5Gd5_p z?Ip16tp=(`BL%Nwej=OIxGU05%Z;WcLGpNepI_r*O+LCJ_g*dlGPkldmioKLkbY}k z-k7hTJ)M8h2jBt9|0^Z^8S;RPYUZSeP-g^f%Qb>oRep&7awNr&en<*mfLCT6*hJ|_ z82Khf47`c~HjC(*7{4F08qu$eSkyi%fyXZWOK*<8W*67aBr)~ZUKF}dgP7w!wH>4n z?Du9gp-?v@Wj@1g@*&Aneo?yexSywGmiN<7Ah-ZD$eFo~FAP=2IW!8FM2^wh6Qc51 zr|Cz)hg`j1)dvvm&-v9}qgJmml@IjhKk~tR1Xz4(^?wk8b&lVi%@!tYYegcP!$>H- z*jELX&jy&#FQJMiYw2G9?G%l^hlykHYnK-DRR#ZFmofjUr2mMAf5LR4ww5j~rcQr* zB^;AD`DM(D68gCq+TjQXS3n!GphVE@MzpMA^drbXQ4xwVKt$@v*ga9^aBaaOXh^$1 zC`yB1g$HFnI*F}Bkb-#n@`ZmVJIeq40!>7{ z$tzVI?>>W;^YFY$S{aq>08PWR&0B?e2iH;(#i`Bt=?7fU3}Vr~u^gk7any z;6_gAxcZZybm%3RQ<;!i%A1&fQdr(EmioC2&09n5M=RaMNZWs8f!2}BYCkF&ifJL` z>iBp@;)t;wt^j$m&B3T*Dv+%YjxjZrpSkw<*CQ9s(It8 zZu{m+hmf8;g0GMTpLA&tNF9)~_3X`{13o=s*D(Au2`XKfH=!*fW`^5ld{!c0N|DIC)t6w7dZ&t4<8c-fOs;D3GOc@z6`sj(RE=n7v zw)siLvO<(4*d>;Ut;!3{Yz3qNq2q_Lcc&(1>7L3#K@@ZpkuX#mDk|S2gA|m~K?f>< z8!55~?DDJk2YR4_a__IQ#+m2|58v+0w!2S0p80?Ax?jER9&dhIQh+hklwr44PS|j8 zUplRpJx?gdW3&EwW+vZNr5Ja^T0By?s`ow+FFEgZZtZluyYvN&FU6_~ZFwgVMKF1Yc-NXQXB%;CX=1^3;n%dZpy zYcIabkg(}JI0sHPm^h2Skj@%v@h5AG?fUlG{Mr(rxxFVw5gs=NN$PQ>H7Db{W=)Bu zyLf#`OHGexYt~79o=Lh6TlnZfEyBi(0CAS-EBu`Y%?Y zAuJ;({kjrs>2h`an0YI*7!7u56wHKpkN)nNaLi*bA;Wy9w9vD3#c1HEuiFzJlO)JK zLyG7AkPy(cbz_F^jpI0!Y?&I4k}TWSk0#*X;PJL*hOIX`Lg|cd+drg&+}HR@0w;K2 zEXzlf@GI+O1`E=O6;e=L?)O@4t(elZUp_p%7LWpJ}W~OAUEfUL9y+jnTuB zxHi-4g9XX=V7CVbEu?;C6os!%SXu}a<+D>7ho8cOGrzLfj6mc6Y7P0pfCB%kLPb

jpBO_blQ3h%7VYn=@srH!{y*Uhb3vEN45-|Y;vAyc zi-}$r$rAQQOY6ifCRRfdrBjA(Vcei`%51Z<@=ZAtY~<-;$=%+CbDNjK+;T|VT=QAc z>R?AG4F{v7?Cms;8Y2%qh|%3*SHG6*QsO&uprU|(mrQ4Fm=?oIYL7>O4Nht8*PJ=T z8O<%BuOOEq#(B;SpJ6Z>>9*@AAo&Ez+EmX|HBceCLpK#(*ecb?b}qWW!IbU;fTsNz>g3)4rB%tX?5r5gi_(kh3&OeKbkw%dYn23 z50WK5?84xLH9B@phGTd!nc|8H!kz3%^Z6j^lTA%Se1BU%yG4Zps8o%=hHy552sSK$ zy<{Tj53fQsjds`@b0N*Q*ZeLm=bH_KxuvDcv+`}{VxR1OfnWW)2rn4r08iywe(oX+ znatFWzJ@;MBSVhLihkq;?s@^bUwT8b5$>Lh!oSPHRHtVBqcL$7(l*tArO{)5&MKp9 z@hM^v=Gz9WPBo~xs1(SmL&^a5QV}YwKtw*Z>@&d8N`m)CNA)*b)pr%nl00YnqCGC0 zn>{U^*&`3#RvDL?=Y0YMm-8*LYU1k1k%&NpxdH<2gIu1Ow)=T6VkV;?poX(Es?4so zaYhxaeKl2PPLU1&7S>omZFS@ut08T2q0u%xQ zxkL8paU>(SFJInK&HZU`W88v6ESPXxv9^ZgE?37S>Yv)MpI;o%{ibDemnAf)TqwQk zL5^c;T|86H_yF`Rhd}ToM{RnY=Q*zho$}cqf@ODe->(;%Cck8Z5uhK#(vN}*@puMM zIVW^w!&s8B{KzHWDDdu$)K+pf+3jqtEEaEqolfwJQ>4isOdzm_pURZs;|}B)FY<28 z*AMB5m1LHOz>MG04Hwr!t||Yhp0Dg=|6rs(?Pso>#O9yOz2DR1nVkspez?hXpcsC4 z$Qb0zCVqGCqA31MXhHwkXjA7;>HVGslZm*Ay)$^$GdJE%3?Mh4KS0%dqS6+?#l-a% zo@YVNSVi5&2JAYzHVcalYFoY7!j+^`ww&+>Z)J-h>$;3g{vH>Z^-k3JJVMUZ9*DI$ zFbW%kh@AX{Snrcu zdPqc*Fe4}1A{1KM;Is02&aqK456dNTEaeoD+89?AGPB2`q@F3+q@kW!W5j6QeQV^n z)`c}q$UjPy!Z~6jHzyqS+afocuU3R>xo94^#FVp&se0%CQ1;Hzk%!y1Xvel~+qUhB zZQC|Gwr$%T+qT(p(&;$4Ipe*3?%Vs^yU%(5)E_lQjT%*BeZRH7Ip>;_n5{j%Nao#c z+2bcX292(e1fm54%WFJ+!c9)3Nq1aUTI1aXGK5tt{Q8qdiq``Lyp~zthhv~i-7x{6 zM*}cxoMrCCPZU~Hz=Cqq>9EvGrLI+}e!CH+T0EeoZCJ&4@>)1A-EsbMhWY3$AxEuj zJifYi&sKEY)2vl%jsBhv6C$!{$Whyzy5gv|?a_{|v&CzC_JeH2B<{cXeA-u3lSV}}|o89v^rFA{xoX+x`_6kYA zHvHIms?5(|?YsM-$2iU!Qq`5!$J6|TF<72F8in};kWa@wj)z7Fz#NSNUnJSTu2Aew z*35RSyrIUk1`%!y*wc7sZ?bt*y*3#9h_1zug1%Q#In;=O4rsl1KW_<`^thSyymr7f zG`2Z*%*X(!`H}#uEn_WhMRXb#uUi3#GAO~R$pLV{4mBloU(ot)o>oYU(?>t)ltdJ#iDmZAo2paR}6WRryPsXzWzWj`>gV^Ld) zFSv+dq+1h4PznrJX7m^R+vWd3{EqgW!wQ2ZO^hpSlOPh9@ zFjuHDRN#sJ@C%mT6RH$`zxr>Y0>U0k8w1YJ>1z9XE$j@z5C;GqD#?4#@hZH^0Wjb3 zrbyB#(q8ZTW0$^zGFZk8?JBtK=U#tMmJYpvNPA%pU9bPo&6`}M!^(+0 z^N;osSDIQ|2GyZgZ5g{UgZQ#ZX}jyutK@!$k87bVnnkSlQiqP)yVq^MI%`CPy1QpS zEZ*y<FZ%A+dR>zvP2wI+6<8xq+Um@D_&*95B^fR>#~vq!fz@bJd<{2{P=xKsNNFJRf)e3>5XcB=qTH?00H+!j5-7w=5H?zEDcS7Tz6~1SAQ>ON0 zAWl_-Rs47bVY%$r?iqpN84$}80k$GKD5S$8`miBW!Gs}tnZE+G;e&V+)h}UoC1!IQ z-GKVG1~(1ZXD~{15=3nT((UdTG3KASGNSL`mv#33LilGWV`f9>1M(ZVLL#nEdX9r~D(FF$l4we?TF#0EF6QElQ_t?l z^yROww;k7SA)DYHy9&3XH+gQhTv_RXCRb9b6oubJeNKO~;ieyCU~2S85RwAUiOu_G zMfBEF5g@-qaH;Mu*Z_{mV|Hrk6n1RNW078d10&ikR~n1*UaF{4ycrhanUs~D@Y5O# zA9m+x@HLen%%#&ex*m6MbGLF4S2~sKESt~1l>EPO~{eCP*| z)^o#;cj^#7ZSF(aY(lU3+7)8eMS7JcUp}zpNux6rpAX>eeY){qA_@N4`10Pjwio#< z`(`Cni0hy&8mJ}}3K=jf#xa{kYK_M}p`*yhrX67;+P`abkR#An0c_Z%XT|%)w9Z>& z%qt?%QW;7r0@vskdlU6s-$)j`ja1vtJ|Vb+YUl-iAj$rIT%`?S_#G9PGlR?!>&bb} ziZmGJb4tEfcRe9|li+Hrdqg z@7Hi&!H%FUiU7N-2X0&CvNB(iEMzp5f?ZSne6w0<&%#-*l_08jM?n_ApBHAY7=TNj zyZXZdls#HhUE-;5^Y@XfyB2VH3H-e@uPNMMm_~F^>q& zDiLKXBR6rr^Lp#g^(g4Ss&E5B^gnQ5fOM248VQekhsgIpDtoq*f30+WDXN|P3`P$L zWS|!CmFkhB65#89h)0iYgaE%&GnIhUM}c&@f^F!&n2Vhd$)aZEOuYyq%yr|++ds%Q zoQ2a`RNl>53(p;P{(b$2*IGtz0?V@N6410Ei4S#<<)M_|7fs*nQ9?Cozl}Aj+*G=M z)R@Q@%T*O*OzR~fi(wx65sA8;<2)lRKjrAF(0x(*Q0*XlNw3e7kd=`jTXX|)8`Y`0 zgp(1&y3S#=Y-fWeL(c| zR1rge@UeXDsCNGZ_s$!Tw~rwWEJCULzSza~H@UCo;=K4e`juLlyew+E3hFSgq0z4N z17BUW_dSpRxx}>)N?{^H1u&ySw?t+pjI~sL+>Sad?MoBTrUs;^2By(8)&+3y07laR ziMof!S2s?R+ZoD1&8nQ0+g<^+XFGQmXE&8CIc~@H=$9*&<;u*;J236ng+i@QUk+rV zNt2sUp=v?FW%m?XbSk;p9F=~!TdYV`*l7z)1!ujZx}~VC)U4+mtSLUmfH=KzbW7F= znmrj~)&k6NuXj3Q*>bci2cyLbxu@xPKl|kQtF7+UEQW8gDtCpuL|s@>7p6?J1mNKhEJk2#u0=?DXnF^yvD1h>xj>TwbT zxqSKot4#CUiuO#AhJ+s}_7E`wi4k|~SPzNeG){RZUt^e6W(d-Hi1JKYksJZII;B)M zZtjkI@LRwg(cMx%;2-_qzM7Uyeja`O`+U!-m+Vy5rF|>HB5yGFa4Q9C$-uUlRMx^i z{mdE`sljRo?0<50_=fa3yT`9^ZO9O{Nu1DjD~NAQ9Or!8y8%EJ#klst+6sZ-kmW29 zM42Ykuz^dzs3;ABlhbl8*iG(bBLk+dM(P_}gqdWjJ&)1oe;Jo_!OEjMP(PUfdsR!$ zC+E5ks2Ivz(P+XkoF-}kw+39e&JQ+5mJn|tMRo_vqYb?9R+r49>119`9^RSK?%evU z%NnUmiu!gfj;7stUS-AxwZ{s3@O0mzqwr6k^1jI0PGRtZo$$`KbwMB99rN}!vKAhV zN*`BMrRXBZn>JcGW+rSok3?k`3ozy8`k3;!P~cr1&f zvB&nVe8sCqv-7^|Z$#H)mG|;cRcNoa(0cbo?pM>oU#NoiIxUw8?)8`BxhAc7I~#<+ z=^f_koyKk7-loZ4B7N5HJ&EsLpV!SSQ;L&W2%k`C0)6V806JRDmG4w6r4?UuV5ks?#+5Cer)(DQ8|Ui|j<3(B zQ|lijlu%nPCh-q#P9;&cwkNzDrjyF?+G{N!RVJ_f)y<~EuqL>|`5zT8Q&a1uD&ur^ z{H8j%Oo6qTt|M-EmUbJk_ULDpf>=rgz(p8`vV8Hef2R5#4=m6mvuVK&0?*Y<_L4r7 z_Nyx$18v6HeFemFlYf23(kQFts)ED`N)+%z_(?^iLQOUVQ3YyvIZO-UY`9vSF*-nr z1IwKu&jnGSlNG~YOor!ki`C)F+{hw`cXtJvooyG$)yc>T= zN~9Rf#Zlm3-42y0APIyA8cY+~fhuk@t<-YM3m!WvWvn?6U}3ENoz`~>JPr6GRZWZ+ z*Z_0C0$p?rgn>ubxm;=3O2&O0K)^97K_rVk$7*Zd!4$m1tR)ky~;- zqsCUR12&m3ynQ8iu15D7)pe0_lwE=@zO%0v^1iTId#7QsR~o`%Pnq9?mgX4RdXw*0 z)BFH``08C-ibcVxKZBdkg7OBlL2%|LB!c3%XCW%DBZhDa$FGifm`*B5utY|Di@sAE zvclbiNNF#@bom>*RtH-5|RoeM;MCZv*?iI4En^9Dbm<5MPq{A2z}eq ziX^2PtpR{5ctI?(Lh41RF|m#TK@v_;>e)UR>^)Wbif=7d6hl}gbY`(nr7x~9Kr9+# zye>jv`%~!6lljfZ#r_96?>&l&tcg>{b#)vUAd-kYXkjZ-lVqsTSe;@? zu=7=NK7C`aXJXaKa`b!jO3^sRyLIYdrzGy3qJy{gq!GzVA;kX(-07_3uitCqpB=IR z<|d7j8=~v_nQk7ej6FO?rRi!(ozN}MkfB$g@74LKBmAVyiu3bGV#Y~uMZV%g{*%fr zB^lWT?qNWN*)P66-b0d0jX$O*h%j%VB($|C&ley*Z|?06*w($Oqw@o>D;26CHgW03 zzIjus-tACkoFfh6nq>+fNDtC)|3c{2Ak{5WeG|Im-;b*QIf2Fe@19dCa{r+<^4$d8 zYA^lfujoq11TC&em&i~Np%0Xm#+9YXl%4j2u56GcV(Hd#K+X5Q`T_$u31{9ZhpLnO ziYpWW_a1I1(^>25>jM7%pTPA$DPXmMFwC}?w_9(x%!ai2-9tOl9iqFtg4XigPE!r+YeN+=Anm2;_M4$1*vU>)-Sk00r2!e2q=H4$G2JC2^f(^z z`Q*Y+B1AlT-n9FhP&s? zqymZEVZ%s+I1@>QUsmTFss4K>KiuIbDx6j@1r>hb<-Z??FbaUiLYBDY?Qm@aofsO0 zD|Sfqrk?+i%$vlrUyVgDCalY zZKsUr{d+U~?Vcvl1PNRR1e?&vkqxU8TO_%ZakUx@y0-1bt2t9sdGZcpX6Ym5*2ara zrGIPCu6Ef>izUpxhP-i!T6`HE4iK^WcSEX8msPL>e#b*4UB8?1sxGAO$!V;!&O6=) z|2f&+1qbzS5hx~#`woPzk!v~mBh};>04zNgEFA*vb0qzkFGkb^s&-lfI z@0qLGH|^~4e{w2>{h%ckD;}mX2LmAqAv_8I+ zh03n1-PYXmx)gi_v8VsE-O$;5gEOy3$P+sVHm3^F8B9I7+qjK3W0HYHm5H^6ZmEb6 z%k2||25wo@h>i`M9MkCB3A(Y!MV3_exPKIFQW}Qedro7N(WD zvDMmuqOB4eJq*2wGjBs-rDo&Pg@u?ls5X+Z1?7|VnmFHS2M4vCOx__I-evt6st86GV^HF0tP+3 z{4M2#VgAcF0|t0TP}<>^1v*5uj5dpM-}5h(R^^eH_?H3FK+)CMw1SB=xX3A!h^fxd z?AvbCCrPI+8rRk+&?mDP)>&w8De}dj&a>)xwTAD&%!*EdlIFcJ*tX~af})B;R`BH( z1g7Rsr5bGUGe5IvJ-tyZHfj`eEEn~$!V8@|m3ZzMs_jfnvfHc!c*}}x*p3jE@wxl1 zzmkI`Ra|tX9`mLAB2nSOG)L_@`?bSOt(fZ3UID2j;?XRc`>C}14Z`!&kwh}SXQhXb zt9hBXbMkSdpk77I5KC1BzZwI)B$^?{^};D@(#|i4D$gKdNHOY(UQp#wU@ZJ%QcYHk zZQ$Wu7z%UtB`s7ekaA$3<`R5D&>vGwJff~6SGt5+e;6Y{1h+eVy{$s{mB7|OK(@g_ z{W(B%EPy^6H0%?g5}y*l6A~EaNV1DNz;x^qUSYRr?k%py82W>TeV0kRLm1nJIl?o6 z+jocTFT^FZVLW8rbgOJak`E&UIylQ2`l1{U#G1d)AtFWW`hXou;b2t@$0QSb9 zz`qE70|h_nuD{PPF4VsfQ2yr`{-4PJRBfFX#S#5xm{LhZk#Y0Mj%)H2e}Upa%UR|K zR~*I{+X){I4V83{9b85!nTRV}yhgvKgs~eMY@D~v{E=|&baz4(n<2gJFMHV8X`1El z^8UUfoG|%GyNtU51InHY*Pb0+cez@^;dto3u&3~)l?L&Kj3I~^80x9iYGKwb zPchnNlCQsb%hE@!gFMICv*o2a7nH0foM^w$2L;(z=koX02$9Dax5j$i34s`cP^c3* zse(RDJYiopy3j+{DDj`t$W~F-`5S@#AP0kXB}BJeI`)%Jhi$Io5kYxzK~x-a3tiiP z8B~v3bV#;$-e}2}Rqq??s5%0rVa9p}Uho08v17`cy?ucFa?}At?oz;}72!wMm=Bd_ zUvOA*MrSM1D&!BDXSV%x<=NA{z*gW|{``aQmOvtH6LBp{iw!q`*54tT%@f$DlM7Su z_qU0MHIT;OJ#B%1Z9iitPI;%{{b}#AE2@q;rhvxb@66-3Z1H0jaNF=CMcKhq;bBla zW>o~^Ifmc8x{N^#YO+BK<_wrK1i;eL&2*~|r?|5mxJ*8WZ>;M6mgR({mx_*s(I~pF zR_!F6io+Z|z0^@ng}gGxTFs^1=+RA!WWEL87r|O-k0w|WM|9|u!?@7wp_2R`G`*%O zEUTEY-0!E`u%~YPXOV2v#|55Z~+jX!6sa?YPZ+gWPJ0lUhq#jcMI zvs`Y*vKfwo&%w-knFv`f&T22+-?uMkFoqp{#oQTD@{wCt z*po(hRCxve_MEG7jIl_ivdk!b#NL(}dwUpCAvE7HYSbLF--iKe{#o&MXTxIHisd6V zA=;EfwtRXU;wII&oBzmu2aCyH&^};Z82^rfkqMGj{vNz9`^a{ZS*Q4dM%B1;{jWr=bF*~xG@qdS3ehzXfU9N3lireA|N2QV;0HTell-xIp9 zHB_1ZCL-vrwxF9r_I8IR9xADne*tPYhA1(Ys7zK~kqBBhFJE#G_xUfBMmV_e2;h5N zR1WXQ51Rj$()fQzf`2nJFw8`O!`L$RkmC7%yD4|MJ z(3Q0wtT}2wYW2F=k;sU5&J>j-Gpz;O|6Xapz$tP;1>fvjx4~Eh_U;4h7@G#)X>qK9 zU>X}Mn$CtA%^ENX+P*h1pNWF5q9s{gf|gs;OSS49&u3O3rkzU-b}NT2FPpPP?S?uL zS(?@jS)5nmw1hquH2Z6p8@krxI@M5a?u@4^Bkk{uufAU&je87is~MKPnNy`lJGoz9 zK{q>757*7(j%Cdo4e;{Ux9^r3a!Jl-Jg9Rd@gor+g?%GuOeB7r*mwK1(k~~GUx+p) zJ&$@N)~4s~IAv~#HvT%jZts&EQE+TzSWbv>YzNG{xWi5@>Q;@wD9e+T4~(mJa?BVq z=V0^p^Ys_u`I6DF)3pxJ#fAnWSh&BWHaNZ6l_Uwbg8#3BqGP$);{78^t_ zP^=VypQAz@L<8!2rl{8+-C&{F5o#zq_?H(&iV}k@F@!6rBCW*VApEgamKhrVWq^xzniQuo^^47V@dhY-j-a z1n!9a#B@x@y&!zlNR zcv5G=_S8{`Y+DRmf1MpJY-PP~kbZa)PUmnXM4gt8n{CF9mdvT>MYVqlP2wTcF{QH3 zw!Wo|Qa6=HwXm=`(C9)Q!c=3itWqB$&uF}o=rR^awp0$n3`4KAN|!s$P{Fwui$ z4|O2tvDHVu>iEmX00LX>MjbTpg_!DA3_0pk{m#15UtzKaMyL& z6C*ApVH^tN#x45R8#ZK{?gp(|n|2{?m*R)m8<9*N2^5Hb#8X{A!5&C<%r_pAWBg&D_ab zm#-#>LOzv7ZsC%9&-<{HLJ$L!nA&mO#~%(q;wDX+Aj6Mf?9_xg)BJN^DM^qfiL zRS&i_V1LnWfc5)!%LAUZSEdf$g+2Fho)ar!)Sh|tpruh`GUBVI)q@#tsX*YaZ*M&D zy(i|0M`tjfA=C#1oQB0Vt1!WP&&eMJpup8(j%QU@Kgl9opI2X4s%dZEpV^K3#xE9y z5Gy<))Y4gQ?VoFvw)&XMoBUnvDEIVXNSB41q<HX>w`vw}Kj{6Q`^-1xt0G&ZAHaqlY33`w~o78&cuKqrD^`x5b0$@p-@UE zRa^o)B+lWyIYDhC*!nP2!6vVb!MqaE*#eoVUM-q}Z@ECZN)1=LSmNQcrX1R;MQ|jP zrCz|qvRMG0rQR4ce>kk?A=ZuQ$X-{eNyBk6*TDwHaakFLJe3@rQT+s4Hy=t)Xp1J( z+}Pjq1B#&G5yh;)Vt8U#mqUVD!(;v4dW_D=TJi6gUN7=h_9EwruWnTPW)@D1xX9BL z{Fp7Zbjs9%n~0sedz<=Fo9Jm;FspN}tDuD?VS?;A17+A&$vZw8KkMqbO4*7Un2 zK2=m*FFp^Ho>`f{%F~jp8}Tdm#(%Rjv>$N##u?O=VW~?)<~CxzCCS(MQPWscKbOt= zR<)u_Xtx~K>_Dw(arXl|B|Z^ei99VzFQ$N)?Qce9CjuSYJa$kiNym&k(wq*RCNU6- z994R>Qs*odV%U4$~!CVEVq)bIAB^UC8*jcYi=5y z0Zz)t6&xs+lM3j}D@xjMF{1S5Xw5AEq@UgJ&p+^c;C{ovdc*x*$lt8$aVuKYJ8a47 zc57M<>uK$`ZZoK|YxBHvt2W%7p&@S zJBj-WVa~`H{kHjG=-=*!PEt1T$ra<|CJTouG|4(*u9V&1@mG*@4_R#R6N|UlpFqip zKsO!-0mAGqQ?t9ahh7JzHY?a)v4zEBkk(JiI-i}FDYVT?+h_3^PPzW&%0aG$=hm4w znnLDCfi9kAplo^ICZ!ytcX;V0YnA-cA6x}CD#7yo4j6dea>`FIke1wELRaGsmVYea z_w>FN^cQ@ReGG2g ztrYGkXKbQ5g7F2&drem6)@@|*;p%F+oc{~#WN0iynhWp@OT56;tpx8%e1 zeZW#*GGSN=K9*%)#ARQ)sH6Db!Bi0sMR*b#391Td-w21`SSKHDC_6y8pK!s(Hf#8v zfeUzEX?W7~1W-)htipb%r4Q4_7IQEI2g(7MM-hFnr^mVqBGY5N42Z^XwPl|=r+N+1 zhWv9tnqyC2$^+;du-%Xt>mkurNVwFOgH#U#qRX(aV8s)gI^WgpYeHFlbSAd+Vx6@d zRyws>>Foh)?>_Y_Yiq?hIfqWtG#xcy8JgrV4@29LM%NARLGMB;yn0byEpGrqIu$ON zR;Dx6();giAQ11wx_YBtca)BvjJKYIww{8!K-lITIQQ4jqZe}Sdpehc!{&rK^x2Vz z0t?FqX^j%Nz-6|uCPl~TM4b)F_s@re(xekZuYbusX%s}nC zU(W&v&#oKU6r68v)X(zNQ|7&8LonrwcIS8XM&}tLaU0f!i^Nwg^Ysc=p;Hkbn2njWu&Y$S%tC!>&Q8YwnXbpbgK-M-czP^YO4NxjVi_!UL1%` z)IF8Tb(xj8%V%$WRpnchT@iEQ%`&2HMSv_P63YPhyC=)<9g?b>Z_^lwZh=Au9i=-&10HC^%eUf{k6YEv~hBWHcD%>z4dg-6uyq$^vbLeBz`nG z?IHKFQ-p(yIGrZr{^g54Gk8oMdNVs(Bl9699c3GbAv#~kkq}8-%ApW;5_Zppma4n% zW+0{7_GBXc{`4!d9rSolWf8B#uj`N#u#i{!{hA4qTA;UjK%SKAkPk>xV@B9bNr8L4 zfh+Pp3ax1{Y@=aqw&|S{=6oFEMipt%Y#lb%ypV>-EMQ*o@ulyBrQ0oMb>(8WPXXwk zVO&f%MkR}H0;c@`7XkC{+^c`8>i!3A%eM$jV@d}m79FKpGnX1C7X6yP-%l--^pn>> z(ua7_R2m|cZaqWnh0N~@5+!OL!aqP@!oysX1a*jiYO06Dd^X$r`uhJdu=eCNN{0_| zQ(e4c_g!}op8peCmSbA$GJPt|x0|39U9Zv7hv}s46UW1kz@C#bf(}Gij8!VH=y9b; zT2@t8@>h%P`VH>dR{P8x^TI5bBN_I?dy4@&OzMeMv!;g#`Hyi}wGed&mIGd>Z~`R< z0L}88r}lem38a%u%`vdj*_(Qn_g)lSnSrUB`tf=9mpf^N9IuFaiz- z10rF73(SCoH-8d)QC5rC0KPGCZTU8@lOfD`55 zu>CB4#`o3BkID3;Epq!Bk1tIaW;}kP(u(2=m`tg|teJA5OuyQbHmwbQ$JkIPkZzK* z7%;$Hwa_hQLcH82%T*Kp)FN$ zjV%bMQA2*RFr5Y~nRalH3^vG~y7lb4BK?Me6F~I<1Sc9ZuLN1jg0`DL4 zvg}ox6FJnn@dD?x%&<1YSXiFVU~5u2kS}NhHSKYi=HM^h;c|tv68tR~Psu?!u zI+y|m(E`2B>(MrRwOKH-NK$1)Els3uv5Do{qx1q}gFneRl&O}hKZYWn*>E9=qJ#p& zVwA>WAnt&j?T;Q7%&v>V8EfT>5u`^&Jd^^r*AzAv=NSidC^1wz*v>u8vl)z8L(&{n z_IybFNN?%Dc;&73gI**PPizM|xbzi9@zlmB$-y?@$0Nhk*I_+d4A-YN4V9QM7Cq=f zx;%2m_Uo&>49Ft9ZPp?Fr0sbSP^N^fBVJcscQm^rf0H}x)jaZEBs8{9*8t@cT^4g| zkEEkg*v*-a{bb9!&<}6~hz1A3`6u<&bqNWIS30y>bpfZ`Q6AudBNTHy-Cy3?RocT3nBv-R? zVeO?o8QzjD^CAO!T-+!nbb+5%(vAbxTTKfCDfx6OSvS>(k1W?To;BLPL6+_2y zmMjq{udqtC@iD25ViWD}O$B|vX7v@Ae!b^?7Tz4#=H3{*Jk=6R2<;G#OU3NzR>wO) zh<=WVw$tMG0&-NL->F5wI@`SiGq;cH?@aD{}N|-$9o{&iL_-0<2#lAs;*PEh8>eHPICc+zy7%gyT2Ql+20%+7?CrKZbh5IL-l;e>XVwf$59 zM#iL_!N_?4o&E`rgS|L_iPh##w^n;==B#c(J48uwcNbHf#O-!rLp@~O1l zw4|e{vlxL{;3ZCHHu1q?bTwV5Q!F6>0u-$#-c?GS)e9Hk~z>G!j7h{NKv9q z{d=CUL>^5YDP_SHZPh_nnkg(k8)i^)*mgL~g5H%UTs6Bv6tKoGa*^@kz);0-a}|-l z0m)J9Oq)^6H}yVBic*oJU@~!#%TX&Vq7Q2LhlMqDs@fo4XdF_X8dmFFCq^_QQH_)d zH4wfK@D|FW$|$TK&_ESqtJx97L`sY~%-5&|udjb-H==SUcx{P_5UEhwb77;xrTd)| z%CO24D1j_~S@mcEZ8V+uW^O?f%J(hvC`(VSH+LEpx|lY7EhbL;YvZpnw>7T$cNh^4 zxW-}i$Wn%OGPr=>bmPF3J`3G$9$C}Lsg}r0uA=;b-If}&@^(%N6eiG1%vD77KH*67 zH4fBrXj}#R?mk(3i?;-+D2_D>;@}l-;Z#zKuqW(V{))%W%Ga~m`oq)30>snB@@D~W zXG`jDUawxTu~V1I5K{j?2k_eue^x+K-Caf!C`Wunc9<8_TNUR2&ie%ff^5<4^|GQe z>NUHg-GoA9Dc%pprTHsfRGLo0odTxi}*8F=JjIL*!Ud>c|{yW6|hPjXC*eYbW zfJkWwQblgRcMbB&;E_J}}c`ZvICN$f?TcFtn4FJiqqhBfq~!CGg#%g6Npxlcf+H@+Tx z&$waV>-$vyw`%@pQPRID<{H54f5gIn>OsB?H!DIf5lB+W*&<5{*R-J5OM|GAg4!a= zX%COzCc=`kn3`DtVOZ&&4TR!ge1fx|VUCK1@emc@CIhoFdbgU#K_)jEo_x(p%^h}6GMIFzK^7S%cXOvVuoR)DjJ zjBdHud73e|v^&s@Ix5lR@>I!SqM7!0J^MXToA=DTO*o zNA+8#w3x`KVP1=3d$8s9^pkBUHzK|V?MWqRN4MU(F^VV{%R3eWWuqYl!%|QV^iPbT zaeLbrCnQB+4tOFVm8vKWRhu~$5z^=kT);1hB~EkE4-QLO7n6x1q4W=`x&!F06idRC zyQrDxZBaW$;%zEOtKfuJ9XJ*=g1)kDmtxeWFP5BDpdRTDD2yfYy&5TKss)Y#5v;eQIGFU!iTgxs!XD%mRG{e@77eG`DB*a1o%s>Xrk~L zSPBpF#7&>O-?S6XyaIYrssl5$Ot zK|iAU@~v1n6)z;MU*7-Rax!zuovfQ1EsipDG9q`wg*+_t1=f6sR={7=U8r=WZ>5q?LrDJv7JYq4dv+g3xVbP6l8@u; ziur>%5N&f*CeCud-`7hz7KuRxdM^;~q~H|0%(>e`C0$4+05~+E)5uG$&F}k2Ip8SzV&ZgJ3+~Fj@1u=37MY7(pL7o=bFW%TJc) zL`>1Uk!Z|rS6mQ@dce0-V5KgypJQl`i0L_%*y5lA78-R~{MY`z`R&xG-{2bL$48K! zmi(NP5yH&}?5`)JZrfjg3|MZa+7Tl*scZ|U16sTIX_cZkcax9YzZM>yT%%+38t#!U z-szF+mUjHSL+;o0RVVPit;Y~vy>Xft6q8@d)MJi7MfW*saCqafthfK{OS>r z>{0}k0UZ810l593S?@j2 z4tJyG?4K1OkWoSxdSmAH3H1LLq-(b*vv~Mh;VJ#C@cb_coBx=y{||0)r2n7y{yU8) zUlgzqVyONLOVJ@h7=E81%J)2{3aYf$hw_K@m~r8cpnH`E8wkQ)!4N)*>$U++IW_<9 z9`&}jV`zgIctfK#m^KvZMNewl8&(>IlTgPU{A7m9AU5-bhrORQFSx;wJw3eA1tsG{ z3Y`c8m}(i?|x3=lC8u2 z)Osh}&&}#1^*Pq(xLuIt`C*Oa;uge0Qd+Zm5)DN%>=K|MzM@*gMf(gY72GyH+Zbat{Oh_o3n7Z0k@Ny_vM zUASe~kQ*AX;Fz0}DB4y;Dc@*q2K89e@!3=faa5>UD`)@~1Pt4YV@FGakr$$Z+qcJMtYj zV?r4#y2niX3^QHpOltl>1TIi%K9Jgk)sEwthT=<^NTNqi*Egv3i{meZ37K7z zHC1P}9fcE+29w@450sgiaut{Wop34z1!UWfrA&Uz9ra&n^JHm*rAo{$+$;khYqyGb zkxa|!Za1W^SX_*h+F@%sR1)#ApubO+cdsD%(HT&x+ZyNmVCu@{+hT7ZWOD)He?4FFhC^@;|j zXVu2Z;4^~bjI#WFWm>ocj2Bg)5h*$2RDj0u;08+Ic}ZWHc5+i~og#?wO0MZr^iQdSula5>ECVrL7CIL;V#J}S83Wfd zR4YAP=!5i7K)Zy|&_GEVrYHm$>9!>(ks7>~4X!1Jm0$Xo%bWLx0CP-C$U=f(A9qZh z>-wa(zc8#h0QXlDnMv}$W4u?(im}e7xnb$j7@Y;QOl_gWi*D}~d-sJ}&W*dA=hiu;4gwo+la2`WJhP=4TJ!K8&csXzteQ+>s|15_K4AJG9Kw+U7S!g+u?JkVJ6J z0U3D-86ZQ{Z0$aTK;18jeiC!x{4(WLW#y&HNIh@L9|4t-Z%iZ6C z@{=y(n-{rj5QHT`?+xabvr?#q^4O#k7*0>l8C_j)w6VK?=eP1?d9nG(j=U)OWIL-; z^VzI1eH$v_vC11#S*TKg+QR34o4;jU%FNB+DGj~g<;5G4^$BanY)x_` zNikAyaBXGVZXzz_bvsyAD_`E%vP%rhdK4{Gh`UiM^M}t%BfA4bmSST~F@n=~a(*_Y z-}DrH>=Iau8-jO(-jqaQxp$xVkrIWf)qe{v%QGCfYWq7WY-9k7lBYPq#|I;OY2lVO zH-Ti*{pf#ncHQw*zhC?!BT`9aWF=IRsAQyMlZ>oNq>C$8=CvhtOGLEIC>&C=F%wJC}@y&%J&8dc8iq_``WWXFlgV=X^eo*-0U(lKHy9Z%;Z3^nCrq ze(!~Se(cLrBC&DiE0!p&)edRX+av3zu=ffk`$?*~N{G zWd$dfTA$jk%HDP^@Rob$bFtv4M%K>EHC6FX%8xJc(ymgwTii9j*!6pVKcWAo)|**p zLre10e)*{wQ4MwWycH-_i|5u~AD-{9=8)ToK--(GF?_+|+J;q522~%HUpIYxwUOdG zXHvX?r&{ZZIEvNZYtTZT7Dmlc^`#9uRe2zIW{CsxQ(Dj{hPO zvQ=(YVgHE|OHZ3lk+UM@De~5xmyY&MyO?nOAlK!PpYM-5@>~>N*BGtZBQA5lVdIVK z+go_d9ZkMJT)txMqqT|`ADz9^b}=m{$J)xKYk~B9)wqr<$E$CTMXq`C>x!_X(B5Y~ zCb3r^9kBD!;BF!Nb7tS&5NU7G;kjH$bK!=iPWQIDUj69Q!k>_&<#FY@0_$AcedT!x zJ3iKY5}7BfD{zSI;97&0{gL)nF}zM47X-?pU461Y?tCQReJJJjPo>4>t1(1Qn#@)Y#s>%^Ua3VE_AQ^X}W7 z&pd`9B0bZp9yStgpcqf;U1WRt8m zUoqAFXwrJ~*PDaBF6v%!mQomAGIf22qw@=mXJvWEk9o$GT8BRGjtXXfEYEX-!*r$} z@24KQ(r*&}?Q(q{t>M!eE0%2vyx3y1l%jKAso0?1v9oXT?}G13=kZle=@r|s#O>uv zV|z7ooz%+;o9wBA?grGvG(vEQ=zn$}o_-<(@A=Qv$cl5@$~EDsZ{Gw;->8;~{u9`% zxc$fbd>wfGmWh8g)2s(H+-<2^JLa47G|o0#xXXF{KHz4@bV#SudvM9Q_tq;Ia3e0eR^wcxg4?D7@iyYn_B z$dc=J_W1q&7|<0m-Kq2kT+{Hg$gh3p`=lL>Ef@Y2^-AxS=+9`1OWUzf@kA{pT|S=j zE%V&=E$5%-T36rMp7i-!YkHE|s-h2(ZXdv(_#^h*O7Kbe+@|jEzOQP&$vMxezuWk+?5RYrT-&Vr*wUFfjo&J&T&8`DE!!{s zH21L-vmn24Tf2pa-|yK!y-aUpwNv|FA4#rhKSE6oPkQCu5ue4Q@p%(hTEVh&N2;>z z)T54Al()MZa7{H!cYbTSTIqJK|M_EeX-g^|kvE3=tyT9)36P2S{B->x=}#6Os-9I; z&fPDc@~LxNTf`Ay#wILe+*q)ipH%;-XmwScaQ+dS@Q9Nf?kg`FgzVTI;?-Z-R*~X# zT+{GH%DdK(4G&|Sia%`Szxl&Qq)cxCN1bMgN=sP1_3XM!@A)m;9cC_eiaI6W%9&e0 zX@eTMi&Ky{x~p93sGc_ehxM#H7IofFTvY;(i_UkS+u$?9f&aFL@gyFhZN-cGOht7q z45kOzu9?n6jE-lMT2i}cZijZyY?tz#oKxpZ-)4yw(g`zRxs#bR+v1^#a(Ln4T9H*5 z?+oLw=Ui~~p1swD`vytVI@?vUrgnxlpUCtaHN6HeQ=P!|ui1H{NzK!u1hVGddaRn> z^kD6UY5E1atI8bOjLoKPD!cnF_E><#nrKo?bjZm^MGIGIs)%UVPTJNf`{R0)U-yt)Ia65=+64dZz(Lo^VaBdQdvN* zV1?>gmHL$jEJHs3mZZ!T>-^bNB;NUR;nUA&wk)&Itv;^5@y4p~yZhAZV-|~d_9REV zG?dm8TvRkK>9UWa+p{e}I^T27dQsmosz$_DvVgQLfq^5jMU_&Vv%z8CM`KG)9Yzo z+VA4cd@AeY&b^FQ&mZ*G4H=MX8I4{4<~*(5Ku zM@#(Dnr%|$HwkZQO|SeRtIFQ}IXVA=Y5dIc;Gku1q81!%&hAbm^r_B^7D)WkvwM$M zc(Gy2GmfQBd@p@i-7x8z`x~O!ijbadovWC7 zcd{++slS!z{BBdC+HsLxw|FhDE!7S^-5tJMt*SF*)3L9;=PteLOx)9#@$1vIu&##V zE;XdSFYh^L9;Jxw+#9cY`M5Ikt0oEYqB)|XGG7!_KbNSlG&hl)F}q}azQ@n_XwC~9 zmo-+YCSD0`3Csvgm1-}Dcv4=pWKUyd#;)4?QhnNYno}G~Q&f5_U(DF4`|$^_hp3fudGH~% z{c|3l;0Y=J#rLo(#Q8wcr^Q!PGgPuf73%5Bh5eIVE@N9#>dZ_eD+DLZFZbE)~iEN4nGncIJW@8)XH_wO(6Y;J14 zzSjDDpWo>}S?q_%pI&!~_7=qY?&~Os*}i{qv+IVw_&;x!Nq^2`@%!4hq(8jNB$MOx zN&6n2C3cA^Hj(wYImID+GdQlNUEMDh9IkhKY39`WCW*yyRXM~P7M}%qg#_3$@@s}T8?Z=mygz#jqfjAZS52z@0f3+ z7xtWEit7Hwt?%@v>6eI3n|V^CW%G=-7Qc()mNTqAZWd|IW-p=?edS-Zk8C5$Wu;XZ z5>}l%tJ94vyi;)V#g-TTW!>Vkg;Fj;Cl;*vShDA{@Z4Q`e40;=%z6JR@}P-K&O8C< zg{6gTi@tF>-p-KBRD2<~p_Ki@`}M)z^|v$(y3J)~I&Z$Yt^20~lWqIis7DpfoZXI@ zxx3Glw_LyJ5weW4o7&xf+21;RA7}S73%As0&JU$aBIbxTr>~4pvuzHrJzbS1BipjU zpW5Zuys52C`u(-8)RIuv8LAvCOPVFK;@+#VusY|n8K*=oI3Qv4P*I1QKu%BokAo$X zsvamZw?a8kG_6`bu|ny>dt#TY^0a8bX#JE{)n`}yBRB)t%=`}~CB5J0&Q!N?kNzQp z$aU#mlNvQR6fPL#6>kw%NQ`B_cznn?XPZOoEpDE@nIXAPe@g7F zB1`E<%T@=}zwrsVeerda;^m^(eg@qw-OR7@_E=e+jnVEUGszSG^f1e;S9_~)G}YRy z_q%(e4R?Rb&v0i>S*;E08^j&5lm5)qxpX17{SVrP5D@!X)&p-1)I@=*bPM&MB#Fz5mo^pqBSJJZiyZXCY<#+36PEMp)#Bp0* zv3z#fV3X|yrMsNjUQ0Q=4o=xqHOH4*+Tw+?A_W>~G20t)H4=<4fB3#pk=( z2Pp|P1+nf)QoQ`SmRmcgmnm)87Rz(uQI^XB#rl0>1}x{kon$%h(9E5vzm?OSbBd@{ ztI{jGM&%^yEfEcJIwBiyU6VgiI>nny|D3U5VRm-8<5Fq2J3UtOJ=XX~b&AM3bsf;U z^m4KL$&h=g37wa#h{s+9M)*#SRcNp_OV4?<7{;JlB1(ByRtC6D|2=Ebw;<}V>hOeO z*JkO}dSZu|i|%w8YB$(KNgsBRX%;#7-qXe_Xzwbm)$3lo$fq1R*`zz&An5!oQgcl3 zjAdI{I8Ri3bUu+miM?}Q=lK58%Gdfwzj>T#>tlbE_|j$3O?fq?( z(rOW8aQ~T=d{! z3|{3=ElBZ>?sn}JzTX?cx9m(tYjxxX`NINPb_MEmV z$^MvMSG89j$}5h%tkT{XdD^q&-V5D7O_~l?o$M9w=R5mf2>tMEfuOukw)Q7vHY!{fBMt>r1B!`JNs|3Soh4nopOPrvrKSyj%Atd z_3)%^%q+c)J#n}ARiXp$TK$#~H!!%SZ^-rGiU<3DOFyh`4+^c;FDP!9d6BEPv`aML zD{tSfcTaz@*5w^IP=B#$^=y+gckUG;TY0C2Uk_>V5p)c{&=U9E;Nat}3Z~@t4m}zj_O!RVH>_WCDxp|Rf zz5Tr{%qJ6A96GYqXpe>eB*Ihnr|ehF?o*zxm|onn(3cXiP3io{7qz@v8Z%U8CNrO! zCA^R)LSAw*x89wWYaOx`F>(JhS^bxvi~(sF&RfrY0osvtMS^>E}h> z`e$jrrjziu`%T90v*LsJoA^(>S{2s4q^j(LS0j4#)|7QIQFOiaSZW9yFa!;ko(6@UFt)WsWmn)}PY4x&6m$jkG7K{DdjQuk5kJXNMln z&%pG{C2l;C z^ZiaK++1y3HH%CQ{U{w}Pr31Ft)QGU@tQSPX_roh?xy|B?^a$~#FXFgDaSfXCewTq zhwJVQtgkFBs{&kSZ3${-Td5{5-RaEU{9K32ehvBXN5h3YQ)(10X3mrRcwc6W zfEdC2uExSAwav>imWq?qu2HQ@ty+Wj{rQ?NllD?_E*E)Gvz3@Y821y)cRqI%T%3#6 z>&RWP&^s1uouw_W>FY4*%K~XG$MUMLyJR2b&PYhzbxDaY^LWsfh($aJGO4rkB^5i) z2<_DjAh9{lqv*4LF+9bWZA$hjx@>CUr}S!Z z`krFt+}R7QEhIDe57(|_PMc!-L%?MNbBE{6SqA!^5v5!0@{4wc2JxRN`oa;tF^016 zytRo1&s|GF&#==+Lq#eVeU6#8%`m2JzsaNT{@Ir{2})G5b~dj+>T)kkq@^_~$cEQt zsZViW)Q*b&fx#KI`^t<`NY>+B4+IT`&r=&S0@@4Y$E!=?C|!vrXmdmo>em+|!)lDWkXUfu02+f=Tke zdhG-4bK#Qb78V#qi+xWIWcw1DUZBq=#VL3rEr7E){*Lyq^5}BObNS_*Ndb2P6AG;h zt)Z>O#LUM6-&!Sb5HwQrm3mU$lda%45|{|umzlj>{4Qw7)>!tcm|~cYz;1IfEevHatm*=i}e!q4_&}CTQkO z0a0xm{tyxaUbyrfLpOl1QUOi{fpQYTf*KH(5z&=oN}}C1D4|WHxCChXlfAY5?MWWq zWcT44Lm7b9xoN4Dg68!Bcm)C~BZdV!I83#HLiF$eNaVLsqW}y5+1_@;_b3R2I-#-F zY%v!LaI{I_u()^f8ts=rfR|w^Ocvp%W%0krOX!eEE)M!`j_x>sYV+T%&V-}l!1-g{ z=9?TAka>UyX~67o(3UtKW3dkuvIiJhf-vz-6N|Pf$r&yPG<0_$JCaG1i8!eIVjCuX zKnVtMp|Rz;HWt#T*dm965&dhu{s+M|)nPl4K^WPghedv!yFJmxl1L#F?OaG+X6_WY zoCycay#uA2 z2DXI8;{R;007iQ@E&;K+Jd@FyM<9GOgBw{9HA`&81aNmCjrpbu1My3nwKePo`CI`O zP**74i$#5u`0+^RN<(nn3m_c}8wqNoar>~y4@d_tJRSLl9)q>NxSX7v0QPwU9@Nk7 z?8hQ*LUFezd3mW*D8zuVAD&|XQ@C({cn65+2AB}4fSci1Fe4&T#9=@20k@Pt1NCb_ z9aR83+zdWGSs0-%Km7J3gT=Ed%yW$a`P_yFRe-@kEb^K@Uf%Am4_aqY7Sk6boQP1uJk+uVcMUp-Eo`1cl~O1+k2&n-D3G=fGw_+ePv02yxd%_ALTC zN*xzV-^4M7lE=ddZ##!jDR3ePoGe7t?U;x~epm++NC&yC{98kS{&O&?r3l8JlUNv- z`;M76zC?Wv&)K%`5V!q6@n`_q{vQ^Y7Rkbai0{cO;9%oirMTEFAg>SU80y;O zON_}YR9;~JChic&@uZQ{%*gK|0OdGDB$S7b`B*3;>cvyUvsdz!7J&SP z0RauIRYh3T)$QO4a7cmC9RgPt7TCyENXsE+D; z!5G`oDo`^&vbVj{1QJU7&^vvu0AUYdLjjSO{4*>VI%aF;?nAN1vqzk(kzBJK@a)Eg zDYoZWcq6upC&hQmdV0vmdH}^1P*4y5@DU5eoa`2$LH0HydE;Oq>AAE;CFpeyhzHFElsmA9 z|E;tp>=_mA;*wdwL@O|XW|+iIEF8^|T2}&Z(ifVvHvc#9kqw~`%{aDqVPTAlC3xIn zno+o80MOUWU$b28xzjfUDFLq-Aj9NdQt*A2mv#B$zogdr{ zO;+oEW04=24JEpHd*LvnnStw)yJ6|!uypi~EZW!`4-TLP2VO4PsoO13H0lJqR$>7S`E zT1gZE)buDokL*57aql~+* z{lf3p_<>vwfNqx|a+yTNB1Su+f@I!huxTNo5#UYIba(Y2Qg9XjG18}~UVyeA(9px4 z*UmW5^xeEC09MzP>y`^(H2}68VJ+VU2bhkFJ8>d#d50@BZ-81B1EdlH_t*^!4$jD+ zupZ2qAPAzr;w$pP&K}um359kHG=$JBjoAYW5>?*c@JrX?Mq2ZCY$Bto!SO$W5u#;x z9@q}zn*$by;!LO_OW6E_FAXF}E4|TDsVxWx)L?-&0V6q_QD-CtC4McihQ_s@!B}{M zc5hB{^>D#=}ek5si%(j^V&u=kB&`0%!@owt{(pRu5>X<==_Mfu`l|1I^?K;GLIERuO^V zaREkkP|N=i`)@oP7800dN`)h7!dH0Eo7 ztfk8e_Lj>tY<;weEg1+_NC1S%Rr)(F(+(R&z4|LfH-I$vY$&gXX~C&4)dPzbtrawC zg?ntIbVr85217IfsODcJl=8xX84>ahA%yC#OBgHEp*5ODk=;#)ET^-E%*4 zFs3@(g`u6tK%^KY9xSQTbeUP1e*Psyq}||0qvLHZbxjRJ4u-J?hnjFedH6p2IUAsi z$3g9Wgb!+*Ua+{L+0s#ci{CsTy_>LQjZVdLslT3LKnJ6Q)d61{XFl|nOr4aL*8n>G z1v*8SJm(JvaeZ%+>nNk7NA-+CyL{JQs$z&LHJO>md$77UBsX?G$V??Y(kH4<$d-eA zCBRBV5YpkS7^L-xZVoO;4MLBwMrxWyr5K#~>!BwJxzzvIFbEHwUXN-0(xWbF5MZms&I*gZh9H@f)7?J$c<(KzRooy`xcHLmGqnI-(uP1&10}%@5cl1#&h4 zmq8bu4!n#D!~=`Q?~E<|35&J?Wm$+^qmLzWsp9e&)M+K9C3IPk08CHZVa&g0`+_jy z;D8IYl9$UdXd8Rj!wKW)8lE2E*_G=&w?j%%3B*UAuIExyS7Q(!7^xf=VCfOhFm8CY z6w-9dspwHqF4aL3gZM}U#EsCs=AiO<@SYxcP>puN9OO8S4lMs)8pYnYd53+ImJW5U z9YfuYM0E3_zhToR^6yLV9L@&fzA(y+CZa2#&yK8hkeUY29IlH-nK!+OeJ6R5ue4@{ z0+_hs@N#LBRp?H)vK7Rg!;oQ0BNBYFnGwc>N`GV1yp;tYG&rjq zbDD9Ghz;W7vYnK4aAgzhGPMyx@Qz~2p z!gcV?(YabGm4$){HKYpshfB4(kIm+SZG8in(V;4p8n+W00#rWeAUI@Y61XIX6TBl7 zls!5VOr=iSi%DIF>`!t4L&Dc5Rb6JiN%D;4m+NFgoW=rBkhVhWl%0^^r81L>07{U_4Y2%*K#m-qPY9f zcF&3I{}w6d7A1flYT!Y`(86j=yumsXzd1Q5+EwTQGen4W=;^A^!+!#3Qb^Ffz$v=3 zn?t>~LULIFQE7CI1p%)X6KQ}692TV@de$WxSfGs(q88=%43qG%Md8^9PCraz-44t4 zh9V1HwjI2nf?-huo`z%DuRVD3nnA6!i9U4C7iqyHJiP3oAMm+zf^Ew(Ccu~qB0}@j zt#2@KhFoGIiN%50^Gs>&DK-eHs1AzXVuB3uAc3c;Un1gs6lSal7Qlum;?-B7WWmsZ zk-M=6u4wwD+owYhg7h4S8L0XH{(wn&K)c#7^8;5^K;IRAep-0o$7GmABWQw@&@CbL zGbR%Lcka&L{pGb4$l8x{fcNc~q=$u%XJn??llk{Xpu8WF6?C}u8nIZWzz_|R~518lW142!PEu` z=9WF+L~mGDxO}{>@^k4?h)X{p)k6(9Oc@hTi|pk=gwbr$2oMgoqL>Y=Xi3dx@D;SS z@`o4tF@n^9*l1Zj{vcxZNO)ruh)o#SK}A=qj*UTIba*zuN%}15P7t)b;X#idZfjxU z4LKzac{P~jvvPsFXkR=){ba8;ChftbX5fE)n47=>Ap$pgRKwEO00g@AebB*#(0B8N zcdBVkqVbIYdVy=@3_IVm0MY;tnvl8e;UtEs~GvX&Ws5ey-e4`+$xWSwNDyAG$OzH#v?ntyJ z84|bSsw|Ftz4r)v{U@w25k@vD9P9tGp>F{#N#+1WpeI15He*sB4C|)uXrGFnj`=lX zifFe?eS#o`if1DXY%{PTJ(3H)mpp^Wi>}aS*I$4V&{E>CBPIeZiJFN0?7Vk>%Qdhc z0q{2T*dfsw6JWrVVQO(gcJ!n9@p&IWfa+vSo^W|PCd%lFNQdY(_!1Y+n&ozO?XRn# za51QkQKcVn{TCPxo4OoknOqMNvmY0qVtpBqAGH(W0IBG>zBUmO1#Z~R(ea~7n1aHv ztZ_kbB1W(_+iEWxu;UF6`ZCO2I4H+JqgfdaF7EM18mdD4W0M@qh1hOPx`S#TC8~dO zfqmA*H(YSgI#V}~j6s7J1KqqF-6;&lb!gqG)fZNfPl2l6!Gl(NQ;yQ5IM5-T*z1`o zY%`}t0|sq3LZ?-y9K(%+zw}TJv{!EhC>Wj_1IGagDx&TYK@G6=H|d!ecLkmaYJuSR z`4A~l?lwfzg&VYl!QFOZa3+}~{c_+8@{uvIMk$Ui9P;wR;ISC4G&Sa{c)=Rr?K5mL z=q_)UKo@Lu$JLxPdOU>QMq_=nyJWDHPf3dwYmE)2rc#AL`dQ!Jh~i&zUG2<>5> zNurN8c31}=Zws1gT46|<00s4!j#gtbU97QAh7aodGWF&_&{PcU*DHr31g#l-=`>v^ zM2!EBL2?{DoJ>2%u>oXMm~m(6q7E=PIN|f}7&vkad~+(Rlsn`K^{{WC?Lp2Ax>yK@ zxX*+OY8GeHJ~r|i9<+3NafL3_Xt&0(ffNWNzU>0rpk4BfZi5dXD`FI>{vSoH4U=Dc z7P{P}0%Ie`{9L+dL&6(&Fms|8{ckQ{?^Yw@Jha_vc^O?WGL%q^TO>^G7ZX-NIi)FVNbA&=_yU`BZhi_|V@CvtS zdgEH!clSO(m{LWUHL7HcIKUgEvc6&o97cI>;A&U#Y5r zF`zo8G2xKz&Vc7?tNP*PJYW696M0p5OdQ#T|rggbpfO}X#8K@!5ES;zc`>z$rZJ{1q(g} zK7_VqK7%jQ*{lb4G(^KVkXE#R*{2G&^nRQzE&nkAq|xfcbFg})_VCXLkn3E*u~Dh< z*Z&8LgV#&kiod^u0mp$-Q5$;9I*FlF!G>UmgL5YDfKi;{fUL0H_|yg;xj8I zGls;lp`k#C16S;>{*Qi8>m!ItXrah5g)uI$I`Me_ed~#ji$I-~knW+?fYWrwaA?$> zm~fd+J}H8Z34xZSqrznp`ae-|B`yCEUC+gXXS$5DqqCwD!JRme4<@<4{=$w(pjY@Woren|HO zVDJMy*f_eJF4RELIw2YI&!3$e346vFc+h@QhcaUr+__F}CfhyQ_p{G{6{0=r$102= zhIK`vXgU$ec)m4V({`wY2}0*Q&=MNL_N}H1HjFc56aT2ev5UxA!;zX+M1Ec~297fL zS#ZYHbdrmeuIHS!c~wH+OD3jO$Uh91~4SK z+Tm&|Eh%2AR0qBp1Ktn4=8h%6hN^bbqIk=-`ae4f-p-$*r!Ptv(iD zb^hZvy({qj6eLCux#T$RbV=U&FoNhmfxVWxrhjoezzRXRfqJg5^S@vxWP@RB8WonX zDHy`rifF^I-0fdL_-7{gedX3WfsLFUXCnbYI8kur3ZlnYAJPsNJ3(w{98%xK0B6uJ zaM-$E?^cHn;Dh!hNi=)!+e?T3IF}kx53Z0DE3~oC6Ci1gX>@*KLl_;%0Rwb*^YW(n zjCuEtUPFR2?#l0GkXtCkKJ@TcGJ*~W>P$Ff*LXT1wH*8xJ^?f)Q?@@u2Lod>q79f0 zF^12B`3i1#cz``Tfcrbp)0nu!bZ{By3L7p4An{4)M-uR$D8Cv;rc&rBMB(-)le4&L&`_~x6*NUC%A{G`!W6#)(O_MDNV l7QLis^lI*nrh!~pgd9J!f*TVEoJ^cd#!gVeWq=0|{s(?!?7jd1 literal 0 HcmV?d00001 diff --git a/deps/cloud-commons-codec-1.4.jar b/deps/cloud-commons-codec-1.4.jar new file mode 100644 index 0000000000000000000000000000000000000000..458d432da88b0efeab640c229903fb5aad274044 GIT binary patch literal 58160 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 literal 0 HcmV?d00001 diff --git a/deps/cloud-ehcache.jar b/deps/cloud-ehcache.jar new file mode 100644 index 0000000000000000000000000000000000000000..3a38fb3d14add403c414d61fc701e6529d069447 GIT binary patch literal 263854 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%0Dhv=?} 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 literal 0 HcmV?d00001 diff --git a/deps/cloud-email.jar b/deps/cloud-email.jar new file mode 100644 index 0000000000000000000000000000000000000000..09222952eca85801a9ab78763e7ef652c6f8ad45 GIT binary patch literal 434812 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

utrD!h@ERCt;y-%RT_r;S+-Wgn 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%0D@xc@8iz~AD1T?YJ!;OG!7=g3&Gil0skInU@FDj^ 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%0AHbiv0?Sy$7=i z&%ZCbitBEG-yOPneswy^?MS*En(auw9s9EX<$=!^kFc8#!0sL8B|^hT4+W^h_M0&R zYyWUb$ zKz0%ceU#aDH|}*lvp3`X(`8@de17eu&g^;LqxtCfgyWCR+zP+J`H}b!#UItbMuxps zE(7r|bndFJQ@*yc0r$?0?DDsczt*aS@>^ivird0`m*~dvTd(Z8}PA#obbY^ zl|9pVK*RTgYL7%6fcbDjDdR~mMOApzn*1SyWTssO^;*xYJDt?!9i}}j^G>R_Bg!RY zsooDwe&@v!91AP3Yijx!-$ZU#l8v?L!mprt<{5tC)7XM#9gLBg=_ivQ^WNjku*4f< z_`L_0y#C@XT4`c*?*XbTskd48zM;yCU>_J}w3SCp z?-$2<#nHr8_Lb=+Shfs&!Q|>RaN+#$Jg|minUkC?c{k8ui=I}Q9O;anHUuNhH4BO| zet0^h>*vX5JL?B1Ncwvk`nRDFjE+me<}zINeEe9Z7_4oG7qIGAa0QI835@W|`U7v? zvPu%VEh!CHFG}4})~hqN<(@~C9~N8n3)aRpsQt=;2`fsEKonmB&9|`n6yk^o!Ys-s zgBsePks_#e9~B@48;UK;xJe`z{4pfqArcctK7{EZ788m;>~Tq5w*N~ps96My4w2cR zG9wg-5qky|O@sP)pNkQJJ@O48t8(yEglvxrb4S()X-~Y>qnsni)tmCTCGYhI79o<# zN0%nv3T_i3f1i`&2wXFb2AXN6SdH0o{>=lZ6<+Z|Xj33*#_9SLp_Kzwe5o@c$hlBU z+2*Sd9pd=6gby5hQtra|G|&?g85Yf zyd&0^{S2waw|XRB zuZC3+BTnn`TjCn~Z8+dif10%_#shWsaUiRq`KWKUr{}mF5AYMyzQ5KyK&c)WQ+=n zH*yz|Wqi-@rJ>PHnvOaDprV^;ic| zcKhF^VdksWKt;V$RSI~9>dlXC${Gq#>jE!!<(QVZci8~RE;RVj2E_0KjT}H6>FlZ2 zrvc3=d-(@k0o17=4W`9d&=qgckat{|`?#kBd;y93DD%DYg(^PqX!|f3^gThc`}z*C zuk^U;(|C}oo@ zmxK$A?UylMrr^hu+ZrTYy98@+;$t#6q+JIZYcS+vc^txKdpK(d)TaJAG|qGM4-TnQ zqs^v3c4?v>xj03K9#kD2+!mr66hn)9;7qS8iqeCEvonss5{Bnb9w2A9(oQP+_I=ot zh@$&_u2RmT)XX~){gt&Kp(P+T#}G!A+k{ifTnf*qR~y2NycQwVm(zoaf;%7n7?1Ot z-D*L3ded`QPy)=89U~?UxILvzwWVOsxMRwMVY1NDk|;55mX@Sn@i@Zo_@rm_u1jpV zs$E#m?1H^c_h6d~Kc>778#RwmBZ5jf^(D$P{1R@xXDjUYZVPs>L?0b_B@aFTNH*m! zxKGf3r9X)W`gQ!>nC;l%N@A~cHho|o8PSOgvk8@JjOmobT#%j<+sOnBHtE3tkb^*M zN(y?Uq(Q9S=XDm|e8j||uQn*k^h^852_OQ#2kl9=NufR{+XQ}{>U!G{(}(Q@=R>nH z$bQfBs+NbqKMu3ouo+Oc8DZ&!B6&Pal9_H$*exTuOxY4t8)sta&_=9@>{n>sJDjc3 zz%ZLg9vIs872kXtz%E`2dCZ~K!Q6N*KzXzFdMn+O;Vp@s-3f2@?JsWQ7JWfyMaiIEHHCY0EX4L(7JNplle z>4cFo@nQH=6QGZ8Ss8YSds-<>;)Bmw-oj@YQ<=UT`$C-OG;&X4if7jsyMYp#3Tg zwRhvX7yYv!R~l=W&l3)VEwG*$FuwrIL2v!qOO-R_x~4WR0$eL===twmKF_*8rWFp` zv)f9mmE+yPT@&~4$M^-ZcYRoWng^r}uxWr6x4l(~9=W1haLe0l2Ep$iID`AXMZKHl zPUy=;Q$@eU z6t12p%~Ve3?n~Z>wT!hoHF^W%w(v16U{zF9yOJY2qhx`9>6Pnx*B%n3M}9(+U#Lk( zCnatA0_tz1kvl640n<;+hc68B-_aVk#46+Y2Bn{1=ZF8sEO5i3B@XHrXP3fK@h?!Hq&cx~BV2Jncu(Y%E0iOSd?m0f|R zf~Bgc4I8U$(`1RYA+(q*vcSrgfs$HaC6+n`!j{oGt+#efY9UZ9cnvesz=~V88AKoY zWoqfC=HC02Xfc*8Jss-O5HPI39ABWZb;4{FY=p)F5U(V~y_BAB#LcIx9pz?dX81J(l!yqDNeqz-b4Apwle!jvc2)paSehg}7bXN@i& zE>y;b^KM0K5a9M3c1~gwX{3+E)+T2z|C-LSvzeq{AA*~EXL#-Houet&qT9=5 z**UN~&i0yTeRE+? zrWToK9e4%mL_q!MmL=oVO57YX3}Im{xBbudks|N)ku-HyFXuV@9qvg^-bIE8^!e3N zH&(+%uU7w@<4X5D4Lq}Gj8Cbza5 zPnrAW+9J@9D4OrCj9_`<9-T|rde)CjWq#XOxqKS)+UDk@ef^^KIotcCAENwZEj#x_ zgD2%wkp70|Sqyrzfj)1lwYeEaf=lpWVH%u39%ew{N~U5_ZP3dW&Sq|Tpmd$fVsSc* zyTy6e^2*0Uc;nZNHAer*I@zmU7pC7lonWPI9NralOG|4;nQpA+IvY-bE_Q2(MThqm z2cobIU3N@&Y&4`>f_d+g2PZ&$ScaEXTm~z;I1Yw6}9^QCaSapw;`< zT*)^fk7Q#s8&GN}C8;-{e*%^hvPWC2dHgU4WyqKZuqsVXp?y)u|EO$*qtBPe0{JJR z-$B1s$`fdQ#P}@nkCphs>$cX9>-)mCV_X}KUuaG}Egk;FRDVPV2CtMsx zz2C~KU*`5biNs6V*SBL7Uo|2a)ZWt;?{9g?f0WT~)A-wTm14LJEb>Mn)iPGxl5OWd z6grVV+648OcCVS>vYl*0_63-|oA3){B zm3q2XD0~SZ^^(^p#SqY-58?8KWKsAdh7XDT)bEh%7e%ehTL_Fmb<#VM$h;kPk!yQ9 zVA>`gn@kt$uW(o~Vi~7LibT#&$o+0flb%*YrzHm_iw|GMMb)!}f&QT|#FNmF9T&xY2W0e$6o4`~bH~bAHG|a#&JUMG0p81BAb+4R{ zfsM-k7*YNPla*MH(?cQ~?f;$XMVGy2oyLmbjWa_Amks}~?#!W8S}z{`wOlxG95n+X zf_6uY*coHZooZR4$iRsJmaq*z(69==uhao-NxhFvwY`sjS9)Ezt~b;^fd^9QGZ442 zxvI+u;j%P|yE_zN9t-8Mad&Q1Fp3Ya^BkohTMzutlhfF;dEYNzGSu^C0{~yPNdqv| zQ%8eBK1B4BDdKFk#BSi#^Ie zK;ISh;p&ghyR=_AuL}GK--Y%e^iMFm)>XYr?0qkw57B;%R_~-4CHhgiPt=X!erWHH z)s4M=aPW)m9qxGg3s&%Z_!Q^wdZk2p#cD7yLnmi327R;*zy3+^DG?v=LHrY;#r1^; zE$=IGW5ed=n41^!z}4^bnvWa4rTWgylVpuv{@5V$u==V{V?xWa2#*jxGkq~69E&htV^C)MaRnyI`SToylr;k0?a>hmOl`k(% znR)v&e9u38OeV4N+tr=!up%@tf8O^2 zgz8Y@icoyHmRpR70Dcr8|H9B-j~tkr$zDwt920O}WBVD1$D1`yBD|o=w(Lo3u|^C> za#YrSh5I*7Sp|(Y{ML!hPqRhsS&rHD{?=Fs$Q3)KTzFKVSB0uheoEvO;c-jX0Gf;f zjq?-3xiX8A&4(piG`i8>-QZ3>3@(uDlc<8gw6LzkL6tA{up8B zH;LZf`8pfn4BG#Oc+V4zhkN&SA{jYL1buc$#x{p@R(!%W!KbW}K}-gp;d^dyygC(W z_6pgx@AEuxc#o2L4X8c#-88b(3*r1QWx1Adg9LrLbeN{b7~BMQWOxT>tFjr#@%H|p zahaXZ`-#yNL3MKCg$Me)x%tKJN5IA|Q7gOmIYHIul3EE?3t@dl!x$A67*T$~>@TE~ zKtL3R9%Z- z?t3BLs!N?Qh^@s&R<*6E%IslcdJfh37^xGIV2dVL!IHSak-Nc#)Qu3Fn!|?K7(>D` z$p!Y0C}0}rKr_Q}J7CLtbOn`akV=l(v?h~OH8zjaq^maHQh#bST6qOo7u#==d}77T zOeu0Uw`}^}&Od7mOvU|EdLCpSDqTdsbRj_3p|ZA&34t|6ePj_6_BWQv(f;d5A^1t| zcQN%8lsv5^gPqWZ+o_n^lWV`<_~6Dg{WoJyVkGmN&h0~myL+EoOJ=mrZ9rf+_qS*^ zOBr^2x!Vcg6P+9|qBP4NPquF|)4j}PD??^fE`V=LHEs%pinU-6q6lsr>y%+K6`*8H zHpOJtFJ#O{!*o29`EQuc8sotIl^0^`wcY}e&Q%L@MUcgeXU=VSTiGeJ~Q)1)E9#TkzyMZe#e zOF$wc7nBgM>Un2gt@9MA2M%|$1xeYFduYD+$ZDKlHi&S@X1t@w>mIP={J?iPib~O{ zsbW?w+jg%g{-Ey&!Gb26Ex#7Xit6o=SzCcD7G)j@C z2W+m`3vH$1i=Mj(*amGM{L@DnM5ej}gHZAA>VdsY2=LJW$4^dE>+h;M{&p{^-8{*9 zCuzX$sJFT>TM~tXdV~?Wxs)9Y|3*=`K}YQIWsNS&8=HAYL+I zFYLH}l`UttFg~s9X+$}eE#uE-k4IA>cSJ7CdPTAxSkN)*yDA5&^VjT#QFrPT?Avav zUY*SbY--HwIY6&+kK!vfe-1!hq$$~U6v)I+%8mb-O<9HnzEwtNJN_E|;UZf1OwG2G zqK1?ZpULVG+nTN+c6|jFeTS>E+>&LmVii^hU?=bUBH~ZZ8xa~#z=|9Fyw^7o z-p~LA>$MmY+{V4PCw#k)cQm?B212;H0bRIP*>}d#M8|;{K1Z~T_HXb-VziU;@%G=Z z|1N9r4D$cY1oZ1y?a%l>%Nhh798B#@R3%0JQ`X>{@K+1!k2=WuhT{srMzmj$|E zk)6+sK9FZkMT1q5p;4@YR4F$~kBjNOOaY(Z0X(5Um2hSbQjA>h4leRz^w<+*Y9Q>N zY!*zyWGxVK=u6Ar>wyV9v>-?uCytk)*Mm<-P=y{C zr6{9aNmk{b>}y!jnGFg9ouOc;G^mOMHlKq;Dob%!(YJFwxUszChUB@|h_-Il_xgtd zQNvNFbdxst=%b=_L>3Gj?Oyf;dbl0xr@iB+wv=0DPf39jIN32^@CC;4gdY`qh;TTD zD5Dj(CfSa#!f4V0Ws8mzDO~l9b*3#x`2YRDKd2iixqf6$c|S6zALFZE#`d=K&aQU! zwuY8A^p>`U4)lV;GXHhdO4jzsiio`FqjUsmQqs|_KNk_=SMOU#-vIR*j@1|>Y zF8@KuE?cABv2EM7)3I&a&KujdZ6_UD9ox3qv7OxPZ|`s1ea_kEj&Xljf52L+ zs-9<7)tocaB(=xzT_Zz1)7Rb1gIRy^K{BWDxmspIjw=e#{)p+&mI z2oCo&v{%^vJkF6f;MSx|fR8xf$kAKTZ>u4hb?9AnACTOX*pa;AD}S?gq%(jn1G zO_tcGz1`8%zRg?r+waeQ$sv@nu6NV9z|QlQ_%Pf@Ge4f@qm>vO@nDLpi3J)b&*&aj zJ<#AdMMG;$F-^05W-KBnpZ}o*teyb;APVg9}aIr^(`XT#XsF`>c(Th&JFZH%+Kwx^iPE(;1f5-d zD#q0rMF>ks0cb)pewcS=fECq>qF>^kF$9G@GPZ$(3~a=z;&xVJRD_T5WNDjEQ#pwY zWc)x&EhZsJ8^r;j2*DPj!T3giqG-gx;&=vICHxk-957H`#nrE+IDbkE8e9L)={|_S zKB(5+pSeTQdkS?K+~Xa5z4LF(Iu|QVE=2o}t*Y;E8-@7s1OGpUoQji$wS|-WKf^9r z#l{Xr1;xjfo4cVQO0buFm7Iw;&5_>vbP4{(qleeSQ|tPig=`GdCMO zBtZ$OI{e{GDm8T&n#_3dX)qu67Ab$mUl~wO8Ep?PQgciIou4AV2;j^(zq{zXz_&8_ zwSCddGmX>iL90iAjz1BVW_0AX+UmIQY8>_&I@(f=pVe>Oc=Bqy7>v*E1a~%SzIRqRQJq2RTn-kO5*SS7h7V+<6K^{sa?U%f;r3Ck>$6vv5};Zf^1R zYCM3^YTLQRLny;+e%C!KOgJ>APG#qi7>~4chpFZIj8aIUAe?58bbO}e^g(1!UC(KO z>h%sAVvaT4qyo&QNf{`s7P~zneN?Rns)&fz#EY?9)j!*5e_4X}n{t2x3X#%#m zxj1R5Y}4wA%FiOaIO+RKBG@qBB8V(scDG8q;v(F$OkzOrk3eM~X}Rka0S^N(Z}DJb zyH=QVSpm!(EBMIJk0K3s7!wQSa-|que>|17(mc5`cp%CWybUOQbe5q;3k?4w`+b(2 zOkwejoxPWBMA`CaMrSN&qrcIiyc~GSe-crzhLDWa9oYZQ20CYbsd0YVoD8bo8=#Zd zg9GFI6-eU%$t=toMs&mIqcnEFLhBxQdj`YbBa$Pt@WiC^>7%Gdej}sx4US#M=+Aw| z@HOBG^NNe-?F?Xz8uKL}`h6&CbdEfcpVOrm1080>+r}8S^ebBu1Q(LUQAF3ggOK2Z zFMLaU$tlOK&s4Q+pS4qW_eM$ctg)SK+^D2oBhoS<02x++Ht$#R2c=S6; zT4`V1^_Z4AQ+{~8)Hn_uHBc(8Zfx}D zYi$yR?OE3Z6t8JAJ=3%4U1V)i1OB-;mhQgk#;m@U))4CF;EuDOly4l@WLI-oEb&RE zm&=MFm{xI&nQ4oTD8P!!364|*BE$hCCeUiuj=?C|ni9cz)7S#Mv|%^c`KFY_on?Q& zfd1>*Y9&I)m;wCbhZES3A7uZr(EUqMQrg1F#L?hiVjjWDHcHt4sCO=ju*;|j6pd?= zziim)<0=-=^A^D(h0)7EXT#_lf=I2&S0_n2&^}mwW(3x`j7mu{Bb6Q^@!u6STM*cR?SX=Y|Xd>5on7WU2Z=QF;38(q9o$Th(O> zL9I#)Eqx!xXw$NHMd#)(C4ho-NAY4F#(L%Uve;7t{+Y2|`x`5J%*xZGR;7H^6KH=^nZx#wgLPTsE2=C^ z-)`amEX!(sIVZ1d&F_IGp^4gkV}plHJV{TC2M6~% zO7{o(wi`KgJxS!x)4Md1{tSbBL8cp79~tFNi+5$D_Z=EP;73LsrsjYTq6VYwTLcZj zEex6J|COOHD^HARd>T73>mD%mdJ2q^*2f}YOD30Lw4V+hw#f^YtY7#}N>e5VmmLs* zuMir(o}^CLCEWX}hcQ*J%Ao{3eQ6YwK7xcymYzhTP#1f1C=ACqa^ti1X5Pi_Oaa0XZ8vDU$fij0B5WJJGfDT zcibL04amv`XQT}pwq5$iRG>_6JfKZWI=h}n5c2wVv7iDqY9=|{fZ%%*!q0dU0L zwnX&gGXWaii&WLq66dIV?Y`ybbfUUD%P{b9CKk?nVk`cAQsex z{(A1yaK?^1@?kkSuw+bj99?@nfb{(y9x_>T`iWpmIG!~#gXNws4x9bJfWq~_o3MHH z-ZP-BlYgHM^-RIRCE(}eg64kO3aC_X&R1`ZekjcmC}(xOdTdF-jB^E4vt9e$psEaH zgn(<)Tdq@qyUSe-8HYg*20-r3qe3B4yE?_bZ^j}ia4-ajTgMz6MGD%MlPhJrZ!pUU z!LZ>6Yz09utI9QzNo*r`9)p>;m#x{E!eb?k>#ADb${Ln(hbzMJp4H5`KkEto=mf6U zRshDY8lloN@fqxsLiW1EEkVVtQ7jr)AVuUZH%9gK^Lx_cGaBzVBxUbcKD0F}4!gPq zwHfjOCN7&`?ixeI)}{wkoCHO^`f4Hx0b5TOad~*fhF95^$=O!vr!VK7ucFL@ zdXph6(12O1Am8hQvqGfddBZ>YnljolUdH|3tAH!V=NuU1cf$hl#}D%VSOp|x1mq=b z?VX*JoE%LIZ2kpmz7@Na)lk2()4AJ5?frQ$2nY#x71E@EEGR<+frBOTWnp&_g^`uh zq;30pjBV4vK{eMSnjb&v>3Dx$>UseA{lJ`_YQ(dsB#5PUHn=p}|ydY_du{?)@#5>(dJd;qr$D{`tn4 z$9#_v8GihU{k_(HCU*3>B;4*rfhY+Hhvg>Bn%{D7-A7t%OpY=(ylhy8XIBg~hh{*j z{;si>msl}3`H<7VYij`7M;f^e5WLInR);`8TAnI0KpoIUwL;hn2kjy}<-HtR_?)9Z z{O$%j9BB`>tp=zF8>0i3IS#o?A5*vSZ0H4|@`Q!y-Ccw$`>Q zou9>flLckA8D?@IFPC4dr*3Coiwh(3#9Cu2cdHLw-{K?xokwJRE`2UL48KCe)gEhQ z)~warKwBj*&T?d&h#3r2@ESI2w%*}hA4sm`(kKN`i(w313VJd-XDwHD;!nmr4Vo`y zXj_#iVV*TMwPJ~H>YCWMgtCnw-%nS8itf#PX4#4s7rRiEv~y`U|5!+Io3ovKbXhB> zot+Azjp42{H?(3wO|?rkH+C619qK%zh$MNd$Bbwx>|*I`T$dO;KgxEoep>VPUNw&0 z9Q*+)Qps-0tPVZIElZx*HdC{S-tYzz@8$~qrhU=hjdRCEK5J5%fh@ar*&fm)r+yrp z+R1omoRyY1V)hv8C2rfk9}WWOS6`Oh(aZJJ<@Qj)FhME0;2pKDc_t};bP}a3R5F-~ zHV$In*qmyJ05uiIJ!uu^1Z@c>M(pjBWtZyDtR3AeTzAMg+OIxGJ-yNvjW9&oX485` zkxPn(M?!Tcp;&y;YHQcW)kcnGBOg!t9T|CGVZ_v~F00jwo=Hp7kBgRK6dbH#UOyAn zLAw!7hHR(#OxR9Sg>oBfzt$IfgN)4Ibj@sSs>>(Uhxui-QlHNF5@U_A+cXo)jOG*5 zV7TfhI5}w<FY)ZRCsjU4ce{cdYAyTP?3`oG_ zPA@meTv)LUT)00vfL^Dk=_IY=iPh75PNvBu2{CC;+?N%~aYRO^f!SY{-YwII{?ogT zcG+hMjV>auo^Y$xy9BZ1KS}0ewu`G>j}U0H_k{N8M~8f+@r=w@du^uOd`(zQp}bk^ zq<+DU3-Jp+*xC8#{4K;ie>dU2!RA6=itgwFfHfL z={D`WwPg%IT!jQ7=OZMvX?QZeFrLVM>HK|Q3dODShT055jJ$yskG-kL9#zhL9?|ZM z))etKvzA)D(LuPo@7%!x8z6<&{?0TK*!22|O3ys2Ocu&^T)nr)GyPb~>{1h@6psja zEh9(4(G!e^6zA$D-B%odq#HdveH5| zZIpJ4;CtOWWy|DK2C3;MYCE1MO+@BD^RRy1qsQwA<@EZ+daJ_-iHCVTz{9l9dSJh7 zUkt6*7S}J*&wA;)65NT%a=<}S)x%pv?WH?UL_y+1-`&#`oRlhlhe!tlD-iq_YzJ}? zz6ZMk%MTF>1Nb_)6PN;G5w;KKm%$&j`kXUuC?HTw6cu3wC2?9I>an_?4Rwgn91L2c zbu8pL3(lo5cPP`nN}e{{hn3#MZ0Xd=8Q1wlJjct0 zS%}%h^e83lW_d;4h#L{>#d6T@Ao??Nr`9^l;~KxDQ?C=xgT9f${yU8FsIE0ZF_>va zWn?r6ADnp*UYNwuEKEgUiYql*l4LzW0w+9Fq~gB4MeY!WNKdu?RS(uI>Zte`>6Doy zRk-{y*yVH4TpU{(`{XH?WVB0!zHiMSf=0T@J?@|}M`8lAPC;0RI0GI)q1&iF)7QwiClP z&MbWBl-|CdAfQWdzkY32AYclraD&O?Tas7mgMLiN1wCDvn5ygyR|SPZm8C~LNvlxK z{Ps(AfL>++q#HzKBXc%ncqRoVx^nPwzwN_UOG_of&n8$*tsk^$;HR8>j>kSHOs=u<9y7+iM5gE7JC! zS%PkT|H3O35W5Pt?ysV;c8%1`a$oEwtcBL>H3mx(JDVpNvAH%1%(Q$8F@UH z9hVrG*o73WK`Il28^ef3)1KN-g)BQ?CMZa{?c7c=|kX#?>XD3Sw4r^VbC$_Xc=U) zRi8-?J;4UatFWC~AtFOe{Sm6x%7MF${);+uROF|LrQ8lOxkuK_a=8% zWt|Bxk{KyX$6~ToXv15_Wn=i)`}WW_WHA(3hQh1Dci;aCsE~wtad^G~RsT1jqW@p7 zQZmlgP8RkCj!wb`P6kSL&W=X^^5SSx){^@_>T4G+`PBvD3I~^V&X$UlsghB;;HxFZD|%&t1l z;Dj##AtVIGwWO**f}_ilU&SK|M1lH)XV;Lwa_H4+_sU{KiD`mm+Yn;Hy4X$Ek8&if z?Gd^gTPuGbskaw(8RI1(FsBl_W2 zuh2kwe1D&dY}gSh4MF}6qB3Ztx*>;7sbt9KpG$V3?c6nOs}&U^b#_Iw-~Q`V6=u1n zAzQ?kaoooXoNKZl1*D=bn6^sOxRrv4LiRO(z=y?9r+j#GkIAA}@;p{fYW0F0ZfzCL zz`UzIk2`Q@=1gM1%P2ctko%lO{DWt(`ob{NROqRWr1FlvQq z(%h!F=<60(knam;f1`M>=D9Rgrea& z?>rQ>vo9*8VYJe^Eixkl?^cjCos<$#l2(4pFF>J|Zk&7@1Sk~|Xc@`^dQMjKdiz>ap-ySQ zAE5An0bu+dF~T7WEORB+CgW*0m!s#2x~Hey+&@36`lks4K|5%Ub`2>bt=zvQ14UK` zz0oUSf`Rc;-Zn50sY4V!bMfu@4=k6j($p(9VMDZ!8<;%%M*Ikk9*q@s&D1Tyxo#8v zZ;9!I^h7(a{vdICA!NwlJ_<_*r9+-2=Lm{>j@^e)z~$qw^UxEF_}GV%nlnDP@II>P2iD(Dze;aFWg@;yfLSI!lf`cL z5#k!Oca<3GL9zW2cFI-z)e)+S6(o~e65C(n3hwCijb9zyH8lCj-g_lj=s}D)61>dV z(f8kkt^W~8H-2*q;Qv1&iRqu{_ZwCx};5*2#>uVV3^RG<&VogCOL&P`Tg z9kQU`Bx=xba6Z3S!KitbnG#FO(VUy}iJNqzt1Etf-=FG%xk6x17xi(Wk%KhVhjl?F z=qBj!+EolFaBk|C03+cl49N>`pPrAnN}WGOhUG5Hn2y=gW9N{>z#&nqi4yM726eb6 zBXVG4v8^!Pm^a;Dn&;O7=4?(Y$c(6_ITECk&>`f><*SL{YAqOZGMmUSgU!l&n*@I& zo`J7I`j8fX!ne55T=<|!e`}NKUu#N+w_@IsAfjmGxOlxV&QAV$K?&=Q*CeH@Sa6kN zcSdnoG;;G%1^-aT#yy#_>+)`D<6Ns!`?5OpcGUw3sny8M_b`Cio0|>fK)`lfIftyS zMB54s9mjJV@k|q#6xkF4jy=&WaT#Oq%*cyW2^lgjCS|4KK4McX;y~2kg8cO`Olf3* zsfg-XLyfK+aLFPKrW4{~a_gUUW86HQ*h-5$ugINokj-DCMl%DyKSw>*CopO$?eo(m zsp@<`TL=IqQr~dEOPWcfRkaA;YZ)!X(S$GVjZ3DR&ngt$!}cFziS)n4()m7u^(BQ3 zh-nvhTIto7KJEst>Q?`Q0Xp0wel!hULBih&^`5vZV_{bvFkFqIZL3{`YG_yeF*OjTm8m^Pyz-KBFJrT(_!G0=;!0IU1CJ^u($pKlvWjlJJ=yUQiA0wY*g*qM=EP8Nl`>!-94~E6@b2b@`N*4P8@`gGhL4^=%O`J zIMud3nacQZXK3je_u^vg#$I#O0wSfAh#`ioYri@s^=N~cyD*>f)dS0ixU1lS__+>x zoMqN1E|CEq;4rjp+vWwXS*~1hJY?dO#uVM%yLy`?;>!*LQid6wwlF(lw;*c}qy)qy zMu8*V!45S$CSf$?^PB3UtnVY2>1WIF#~`Y&)@rGo+D zVg~jmO9kS2mdp&>Tg!EY0ZQ`n3e%x%pJAQ6=bxpKD_l_y?OUiqXfAr(Hqn`yasy%5 zyDpXJyC@<-GxM7D_MuU(yf8W3&LyBPY@4M0SZZ<^2Ql)$iz%j}(jPOz#KcmElZhtm zRmLr%LyA;Ec}m>d4s?B86L&8i8kR0})dZUXOSCC1Zi5qe69g>;;K+_s&~2{oS(+(n z%Z+&-_g}lwjIT;4{tujiKQK>`Mm8HY^OIw9gvu!*U&&l{3b(Hwg;_j*;oR-FY zA`^Z=XSru=dHRdb^5ceql0Eqhofy;9MviE5p`{V^*>wtj!A+(3K-KC!e&z6xFv>Rl zI5K~rRLTGKK!X%aO4KbKfj^UJvLQA~_JW5fAzUEBRc&73T3Ynx=tjyW2Qc{xsKW-5JIxZDtq<|R_yytB@oPRGZRe%fpoz!&TJ+UYz2p||B zs(##|{hcrRDD)`wu9X7<|M|l=iQ|+CMS)R>iP=tf3ubivZJgSVw`y_5QQu%ybbSaa z-=mB;`vJ0x)`1-Vpb%RUx3ybR30G{3#>JTsj7eG7RlGK4A6vv60=HZMFGkssQtT%& zEn=(*LsB@~3%R*T3m2)=00-PAODq&G?wIG({rSnpQ*iEJ_$`EsCJR}kFIK4hRN{`- zjTK$M{;)ffD)}0V&6MO77GJ)hM~Sm?t>ntHCsyApeNe&L*HfSjBYWKJz(a$u81hv+ z_W;@0JEAF~7N`oJ%@oM6$>WrZU8%?tv!q;BvOF_}r#?P_V+Ym=ifsK|c~k%wy2_|P zge6TzJ!8w(=ik;1|8b@eC2`l-!T$J>O!WWtwFT{r-Q~X#=Rd!8wg#k|whHR!v~dD^ zdZ&}b+DsDNX^wP43ABV1RIsVlJekFs6CrItV*0YviLq%~agn})F*2fvd?Qt6ME=;E zVIrk6grY@L9!)?*geI!Cy?_Yl>>lS;SNb=_XEdm0d&BjrH{=WCq+Me@GEJ0m+|oUb9?~)_#QdihVd}uU&YcuY#8+1@#mFp5*m3rNPKdaZS?tJtC z@BLF|e~Nc{i>`;yE3KGN{jQ-ZD0W0#e4aJhN4FOKUnRh;iQ13y4QsVw-0ot4N%Lb?6kBV@EvhH~JAY32sd zV&$wN@0~1qX{YFF$k4AlvbJod^2C;oq^#UpLYov5z*?uSww5npLS8B~2;sK}_n?rE zP68e>8oO)J?k#pJJou-*c4?~+$);mLHl}Kur{qB!QX}joThK1AwaZV>Odk`6bW=+z zqa~ELP|D9f5b<-F{8J&*f+&kk%B=~UU~}Yq@NY3*a}gvx7@M3(a>WymWsAm)vP5&o zua0blFz)V;z`Cbq>YMU2mh*Sds_ZkP)`1Z*q}xsy1SC0X|S#$!LdkmD#No>%t$7jC<>Cl<9|pCUvPdE}o>q}b%V3OK`Ve{Ty&Ey~V3?kOGV<#0B7P!r za4IetEf!%>n?e@U`o{R3;{(Q6j?P|B!ycS=3w_mZNU-d;5YPIqetgJwmT*TjQbvDz zG+ES5h>Dq7slnpsJSg&ZWv?Hp_76D3h15z8IW4=AK84v)BTj&f4EjU5hf#=Mx}&vl$Kg>8)6EE!7eLVz%Gtz zJTS48El^zFDc-eM5H@G6c9ty&TLoKWyY+NYb^Fm*xJlzQ!#`EJLBLmX1oczJj36W* zDx~v{0WZfscu7-{+p>}0s8dPC+_sNY>uAas!!Go#>N{8gtccP3$wX&2NDX{P>7w@= z2CV-oTNblTX_#6oOWK#Y{a~+^8{P)Z?nV$&g=<0%uIn;**u?XsrWMt-&Syty5Vac# z@l<%P|DA(p6IR4BFPm7YNHJ$}Ump>fp3PfT=1#fJjkHRm;-TjtZpXLk zm&j4)1}*LKghGd7;hD{HBd^EuI6iQ&B0|)pN2yiIQ1naA!6Tb)V{vYz2zP#`6VHbV zUtq^0%Oe<3x9tm(MiL>lt7*>6(U6T)bifkHPx)gL%IK{wvPE|3J=YQOLAEP|J1F%~ z7xy3=3!ltd1K=vm8;xEYS(D*QCwnvDgDE-PHYhPnoA+JFJM)U0s=8rewvtDA7f6cR|99|N8&dO zN3QYMU%R={zHJICR=;Bm&wkv#=Ho5-)9*XbS@WJ_TNbWhim%6% z;7tMnSO7`8K&5#*q>=cOkrB5IA)H8Rn-Kjr?TUmwA7&_T(04=u4U|ydO=PB7&8z!z^fqA3g30 z&L7;Ja!AdUAF1Iq68BJ*I8q`62XH`{e^por4X3SR?_4Oyw8KIxfEg@df#ziEb)>eY zO>WX0cugBJqZ@dTcBaK?ge|HG-uAo;#x;1yccn z+vMM9gVBe5#z*F66sf!X>}&zV8GaZC`>^x z>nbx`UhN8(3OQy^!CDrdG?r%S(h_JL zNr!WWRw(0O)Ui(v?($Tz=+uGMFUe{kf!04km1c&XT=bDWqj^lc*-L+Ej02$7 zW~ZOFg2e94_M(xT;q>vV=1qQkAbl? z)N|?<2e9sl!rqmxbh$s-T|;B$$g61_HwKR|@ywrUJ(y+aYf2>O^-S3r=OV3yJ=(zaCe{DWTNHK2>e#m~c%R3C zS!Sm|pIOAg$B6Uj;fu9<6G!vG4b5M$i=OQJJM8><;k(Ai z5!>^kHu}voAYhkQ%^>AbU42D?mALr9#@v;-GuB3_{el2j%B-ivw1s)(K_8tLYyXK! zcM^X;VwD&d?x{OAh0(?j7*w>yTJT4Vh%Qk%Z#QUae2qrktQJUvT`f)c?L=kA%#z-RvYfyA}&uN3@AVJ@?x* zx_U;cQG*}7x_Sp{bBG5!+HzgDHazsFb&8-hZSxS6StyEZd9_K?>fmbs*4XM`R8uiL zUpl&VauT1P&8uO@stLB01%zCl{C3-j_ONTofD6~8vzM8rwOMG}Q8piqxr{jo)dXLj z3dsK89Fzsn*aI0s>ARc5R9^p>UQ(zA;O^$zAW|qtxu@8NZ?YJ)Y7$hLOm1F~-QEFY zGC;7wppY|79Ke{3#^{VmRR%O}!Dv|y<=hC8I>*#%!Re(O4Y9Z*sp)@qqP-zlX-~bl zD{I*kxDZ6~dr(3%7Tp|JR*8Bmk1fT3dP<&qt58Py7p}6nR#{N90;o-Cu~Ymd?%B$k z^ZP7SF@GfgAlg~+_ShDqj2&Km8?BqH9U@H@5Y&T9PoGa_O$+dz#kk@?&;F}U?!UV- z{R6PItw&m)f6oXHzquon|0_I}H32x882@Xz`d=7XSxaeM6yW&bU3+5|eoHS`PW<(p~p zH1qv-gATV_&vnBMe;wkbM?W;rI6yJ7GL}8UD)`}k&4X~BnJJOlq`uM}YK_fxBFU(I zQJK+v#n#!UWB@_V6Lrd-&8mZExhmP+Xp)B87<1GkRIT+izOg(1(Ow9%l<74O%+@b)I8CiuOTG#zs>g zbBaj{W(dWiNBZh-_D!l9aq6bG41^V*b*kG`H+>sgu7wiwFLCmsu#6(g6@Bq%l$|sc z&B-)E#PjBoq@fp?wiEOs{4AAFEgWL_lN}4XrW00dG`;%4=HZx&`x5b`+HgA-?SU4o zA$x&-*Vc+%PiafH7)n_Fopvx}6WICI5(CxFtsWUz$_b0s;B~A^h;^L_%32oXUtJ}! zJ(|0{Dc|d>0(BZSp0Jj_t?(gM7W2+szsN`0F-Dhp)zBLkbi$Qun)0Ch=rPFTdX+K<~EGf{u~qp_0&M$PLQj$M;V zU@CYmguPzxPW%t@e3m9Qyy@E^N=;qn)4O}a-yB5q zW=ZHqT;=O=Bzu$SDjCCEC3eCDAMldc;|sa@A@UUcjb(@dl{P@ck9(4>WLl*IR_qXD zl(ys|jFhkeN<2wi3r)f-#h*?00@}2RK)VS)z`nmQ+Ii~#{FwVxew@{0G{6_~FiI0h zu$=uJR$ctLq;j?j*|cYjn0#UxY__CgQH#AODqP&!nmqfW$&SR#luPHtF*=<6B6RV) z>{i-lZPF#yg0CR78)nvg>s0iP3TbjPv4nFy`ww>fw0THdSl7Vz&gSKNu7~t3WZj99 z7n-VvAP}vA5`BHjRs`2Ual|wu{Rb7}E1yZ@8S`Fq+0FHTqno`z zlMyz4mx`C~=RcQ>f6>kUVXbOx@egO!f0hjAm^!FF28^Jq<+U}<-bEE$C&o)zMFgmb zfY#PQEJ?|Xi12`#qn&R?sUbC7fLs42?C>yeP7Ajm5QNZ)GDS(N?5bsMc+vaf$G8(@5EfOuP+32v7Ho7&2gDeRybaHD^?xUz zRI}Hsgnhqu`}gxd2`Jy(PDK-dy`3$?!CS@(;E$junckU^Rf^ov;9=vOR?9K6fYz$1`d`t`gd8bu1nPLzFtJ(WlC^%MV znZn68K}e4QTT935sv2q#5eEXv66gKJzy$e4^#DM+n|t_3tXE8wTEW1?z)ZvV|njj5h{co*0=kLm2hjZgB1NY+x(l=%Bf3gO@-$mZh&dJW`|61ZEt3$gb z9y|NQlD?%5+SS4irTI-RuGB|Ef`noS*0v4qtbnY`XNC|QY>N^|i6$Ut={A_y)Hk2j zlU|6=E7mt!pP3LwgThi4X=JiVbQEv4H*FSQEH!nwZnnRvzUIjNG0{lz=kdAMnSSZM z>pFgYYs&p&kJ-b;xi{S9)`SsylfhP_^_+pfbw9S;^a5=2DWA#S_!7dleZL=jJDu(dZ1cGh`=>|m@6eOet2Z?N^?uFuvXAm!X!|=m*K0f*fAe*V z56$5A>Fu`GGdStH7=OxcuG3+z^F6VR_%kx}*WNJJ`$DYdyK>9dVyG|mk&a#20ss8b zcGC;I&F6fd?_}@fvmL+YH9y+ta;(<1FWRTxF@KBwVyukYBDp^OUMkSd$;ervU=F>= zn!3-81fw0&AK7DbqdN&j!%=O1mr))mN%x#24Mp2gFI>8rl0JIq;snDHqHI-*hLwcO z_Z506r}~8iOa50UM|I0ajgm=68&w2j4G98cRA-V(+VUpj=pIj+6mh6A?pW|EXg0Bc z+1;ZYV;&sbEgA~R0YP${%amf*LfQJZVH$@9`x#CyHk!DfF>*132{Ap$*u>5v8C*D4 zTf>0~JX4S{6vO5`Y#9hN(pRyh{fLSKqSQVNcRDe9J>!rcQ^0liJ>XlIxM5kCG6=2h^&@Fym90+x=t zW*mQ=qN8z~_quuu5yeEJg9GA$d)?HJFALb+X(r4zSdh)WZ=At~h~2~7Sdru^l;Jr! zvftVl$|IOEA?2VzHPaH~qpJn&o$&H)8KuqjDybS-VD0V_0vs7Ok@KkC4E%*XcrFQD zO3$8u|CamT?(FOo?QW#2;{7hNbvbhkk2-LauHAbrN@Bf?8n^_QYBw@qNri+C0hS=u zWH$3xr)9Riv;Mg0f|{Iw+`*e*3O&v4vj|s9rj=m0mbo;P%%{$f#KEU zyIRjrxk1I!(pqPmyJpA19I*5`Dy}q zZLi;mSeB)&jZe?3SunBGJ@97t-6!o^Qrv_9>D)UJT~F&QC#>JC%aJS^+F@u}LMyKb zRinks&ZEDs%I0DD^4w;+?6Nw8cgh%f+?&Ua0j-$o!l|lwzzA`OK#!;JAVJv8n1!i0 z&e;wLeXWL^MNkx9NQ7r~eO2i6E-OFiv)8^)#$xDTftfPEjsgc(v=5#mWx=7D6vZL1 z;}1C-8a!u5AC{36S#GQ9bn^{dr!_Iq@DRrH%-jZy;l58+y#q)Qu5!HrP6OhQ12#EA zW6@TMAIl0y1VY<`iIohMo>f9CX>>Eos3x}1*qbho^*-`*QFQE{Q3}g?JzVS>#xO8j zcUUUsYISZLZQJTMtwL_rBrqCL*rrW{ZEcPSyL=wo!Of&=Q4|Jdl-iDfbDblyG}$6C zIx)|aE`wHpxmw1ySZ5V8%V&oBfDIc`-eC}N(od6qb&e|2W|gRAya8kFL zuAvy2e$_G6Nn`H!hb*>T|GIPIbV|o!pAYySYIPr-Kg}lme4{7{!<9Ct@mr(6Pw}#? z*xh+oQ}}p{#(%C_jS6UO6_`Jb`_YCvz5I^Y%iy8F^4%J2V@`uDBJ!2 z2xe`X?}aZla0b?6N+~hytUNmz(L$sx8}B%0Z%S1)Vrdj)RuDR2fXZYYPHeoC@0de@ z6B$CH**`W$^~u8hc0~ht>uil3ttA)HXPv;p!JoO>#{0OL`>STW{kY^LAC_+W3q2@0 z94;=+I8tbK2Z}E1bj7Np=BhbqGeH=7X8P=t<8ci3!KH^o2_s?%2|n(C@dQK>tR{sJ z1JyXEf3;{ia4%##-8XPwB_r+IR~(!60rVSXrNwHE9boFnY{te0SmHk)Z`xu!tCC!y zUy>f`2I?#OlAY^<)~BKrWK_e4)b<16L8h1)LT&}I`vOIg&7+uM_(@&s1l0X2*%fHe zpQ`#5SVaUm4cS~>QAJ%v*+3_V*B{9?dHO+npc67F;TdQQ$RA})DGk3h3>3Kx!tEj8 zSmBxk4Pds*Ibruue4H{>>7_QMR7=IDEy6F-E0<2*m3iSqqmNBQV1x1^J0ZeOiK~p< znhf$grl>pp>$0tbij5~m#kOtRwr#6Yv2EM7ZQH4!V%w=N z@BR9Y?tZ)P80U;}e($x{-e=9V=A2B506^MYP3&~P7dtKC5#G7Uge3zUTeqp|3nPg3 z+DuAHVQuLyNI5Cw=-uIw)Gn!;dU`)HGObGg5V|~%KbCAtoA<<6iy;UVhgRC5)&cxj zOL}0_M@p?|n3^vC7TBWPcYEFd(<+5Gp;WoB3JN=(qD|qKr#-c>^|$Bjr-dK9`iPpT z$s7T*6aoIJYMyIX45SaCR*Ev-1$$1et_hSm^%ODd<4)$v6N=JU?AiTBCl7t*%&-ak z)LlrZ+E$q$MhIPC&rvuFo;WP-AQjgHKkp1G2NGIA@%wDKMG{`ffjNc#ys`JOOQ~lM zTW}Aok-_TwGXr7vP9rZqVJ2`&9dFz1aG#|svxegL7FaLIr%M#v2cdTEF<}ZsOzxFT zk+`P!b_rh#yr)lWiduH%r%sB%Q83GNE!wg2iY55*N;M5(N<`dX_-2hj&ZTl!Z}CiD z;k7*KUwY8RUMVhnd`|xLRAO%3`SN4AB=hpn1s#&@@?5p~NDLg5)D;+vy;c|3_68&~=++*Cl~~N1k30^(VC^fnR4R zFJpc3h^lybJlbLjb8_CBWN(OpdW6s1pStCp-RWdfsBq+M*kh5T*h~5S=S@mkBy`9?EPT$qfab@D%2;K<|~+oK*`rQplZF=oA9YFG1yqWOqKYdO^YXs84wwPEIJ^mKsl z$)XDWo$bg{gYh2gG~^yNqv(g*9w-wj-;&f)T+1>0hnpL8n$X#&ouj7xacpVt;fo#fhr%_ud(z2k1iLafsftl4VN2PgFdU++PE0dwSR_BqtDH?4)QJ4I#4>E50tky|3j3!M( zk&#}7%jGFTM)`4dab6O7oNh=~Nlkb4hmJ}Rg>zQ7rv^f|XWS}_A27{j(3UNKb$#}M z(gcQQihjyT#&OyP`{hnR;}z43a-Y`^Uf8l=IZJx6WP@sIv9wB2z}*aTnMl*DS`j?( zbVecG)cg;b`OvBPNE=5c*RiVO*oRfB7Ja@EpL!&SA}~-#?C4QboCF$n0=J#5dbq2& zi|5?)UX*$ixiQ*;LFrFWi)yZd$1=xihPCi0x2+41|}fI#jb7#yU>$lGb-M7Lp; z>=<94IE%pn@&|U_nZg|tZud#@##nN0%lGcko32G+^oUJ5P9TiI7Jvqs%9EjRFinfwbHM6Q)Efv3zyHsx3NxhrB5A=tUVIox; zP96rqDq~;}W99)&Qra1%!&*?oe9!`+G(M?xHZ@Z67nQ?N1cR`Y($OYYKoJAdoM9+u zD2KF_1KmMfN&`MZqxVpfIh>FqtO$9;QoVAxa;iAueosvu zE5q=z>S*Tz7SfJajxz#WUUd3?DuP98oW0H+d+I%l`L3lvdpk(dgPzELnXX5!L5Ud6 zgz!DZ-RgBIW9wLblHM!kl$Y#j)N3~>r0hn_6sisScZx+tjWM30oicDDp!@jjDKYu} z(M#&1D@W+f14XZ7&kkXuCiSfQ73QBKI)7E7h31d^XI~>aQC~PD`B!EA?~+OXQi+PV z8=2Vu6Hf?=pRrxwM;z{v*(-F|;v1C7rJ<3RbjX5q`T+wcPEG?EifFmM*)080 z@f{uqDW;F<$2aA@-l7&>RKsNk0&;oUj)n%No|o>X8DH-=_#IXp6p_jlgPri`>h#v& zDCFvt+A(^RF!$Vkb^6L?pQWcggX4!bSne^1ZC}^6fm)0R@!MRgeD(6{>oHnTuW|a7 z{I|dmi4+JSgUs#+zD=qgzfpPXad}b%jN56~l)PC*I*VaJt>C?vT0i1^P+wH};(1kG z1CG9vOcI!}x=2jc5`|2I11o6OjboQ8GR8G60 zy4!SJx~nK`15ey}48Gaa8t;?GWHgp&w>xpu`+DNImS#|p2emr6fvDILvaR}Rzf5q{ zpAF+$KkT$o+CNIDJ{!VO)J)WI?`0vme|mHAK}L78R7K{X$LR$Q7b=55hKSRbMaG#a zM$Fl^Q0P;91TK{{-Taq5ep_@2;ldeiiUi3nmtwIdo39jFih4@xaIBkcy+YmkiLgj$ z2c%z>l&+U@3!Rh(M|@lOqO3F~2-viI0)q6kTRtH=1)=@X+HCaUJDj&OF!W;}R@IBK zc=W+4)_~}EK!e{f+hUB28Dw}fYONz_^kgfRegCEd@@bnv>;E-nB!c^YKo|cD!~B2T zsY=buTlpdB&nM#yYyy2C@lQ;_+}tr@WK)qs z)ryBYO{vY$Z=5kvJmpo1-_Yak6}RBgBm9{4g{auAtm2_TX?(&;OpHRMNIPT zi-q9p_-a#p|3hx~M*>d|c}wqcmkOAAP$BK5{Qgq|Pw#H`Y~GjXhuxkQ;d3RzujEIr z?3PMCjhjsFZYtOwvaYMQYf#Mh*$}@kVn0g2Fdi7-YXqs%p9`gDd#BaUd4-m!O1{1z zDPgUPLbkoWjvy%f(aLCLC_Tg|Ebs8rnll?ivq~OI4z@4~i6~Bm+)9d+aZR1DgNK#d zvh{dV0L*WuJ?iR2eWgoem^MgXOU+EK(P^VS+gz<=l7g~ma`-`Qv^0>a*(Kd5|LNXV z$F2rGK_%s~ZSCwlK0D`JbLFH+;N-D7#Wqx`!;8y{wdyLYc}y5Lsc{hH=3FzV{PiT5 z*}A08W=sH!tAQt*o>NThHOg78(r*=UAOW;)_oB{Odw98!%S(p4Kz@NY7RO>bk~Ce+ z@MibWoqEYTDNbSz3B0R-TtSOZ$JmN9O$MGgq)9i464Gd5cKI_^(7j#!Y`e0KHY@s~ zD3Z^nOa-#?7n6Qry+gG3a~#4EbV99%CGzGi=7(2Qg>|A-rfofv8rypgc#1f=3P5{w z>q{hMPhx2P=T-TNEPhy#TY{sh8~0LHI2^{kdKs)|d`DDZAjzBcX#wE{rg*eu;fqpm zomS-vebxHwYA#1!$65UtQqcHoupon$$}d%W-G@6d`k7n?oFm7jVO~&9nJ8cZ781bt zUBII|DOD&O7c{{%Eqg_ELxKgf6uvDRc>UPDrBoQE=Q{mi2^9gJ&dv{pUv}cfVul(e zZcSSk61yWs87IS;a>w)SnPhuo-iyL0WpmUTm~}|ibQ57`Zpe&a&Fh#B2OMHYn~XwN z*0#zzzgCW%oz(s`H71;Of{rb2TgM+-dI)?MS{hl8Wvwqb5r*$$GdA{e2BwTLQ`f4$ z88nx!TtZXa#-F(#{Ng$#r}1+)(8OW4?x$7Lw4a>EJEj-}(Qj-&6k^UeU&$EPFx1}w z*CUZINMRGDb(|PbVj`QY8AnUza&e^N(X$ScJWMzO_XtwBkcpINt1_5c%?j`D1EJw`#|Vby(vaYER0UC|(@ZoSLe9&2Q$Bs?<}!f<+UG90~_tjo*Apld!C1A}NdAgIaDb zD5>J|5#7%S&$V71R%8UKv8JlW?Ihw&o(+g;I*}}r(ot@Fkyfp|DDs@AUR$P~aCKqT zC>7iw+^GA(*6wzz$OL0cf$fMNkF$IsSz*F4Zxg{{s$l5F`x$N&G;vTI!K;5$?UnuswN$Hqx544?ohHuunVUPBo&&IjrWed4chConKL?d-JlA zxi-;;+zc1DAdln%O@g#S(OP&E&7AEoW=cq=aC=YTPO?G zwT>?%2ooVzkUQu~EM*1cB5(n1ZAgqz$n4ua%bMc^N3i#rfSEJP_WtS4WjE%Bc+=8LpfuL3S_;*H0kibzvFie=350Hi`8L^wGA2OW zj&1YNE(q+|g>E>bjI{R9zy@*gwiyx^86H&v|9*|NMyUgi6Jh=aO?i>pVRt%m%z+5u zK6k&!AeY8(^78veF}a1&J7etUW|3E18NHwuLs>DUp-revKSTnWGnBQ09qXvKMALnh~!!h8? z`zYyI1jZ)2d@WnkZ`X*mg4kC;^+zz4g!!l$V+dgMO zoe~F{Q=rV}0+)9LRh;I68s&iPx0G^BFKm{Ovcm>tr^4KHW}!Ld1yPH!P)w{UQL!jE zz-$ek>02m~hw17VQ@~uOAbN?ya03*^(xoBwk;f6yL+aToYW4`!EqP1=3Vim_8uu@= z0b3K|(T;U)>!M)5y3aZc*z64A2UMWLvcIlX!5zG19$k~T7g06T6JU?J;Z3G07?ogqKH;k{EckHax~jxiVljIteW<**0g& z%XH%z_DIV$@heuwA5Qh3BpmP1QrDal2iVclTgFabPnWP$l1^t_Qs3`ao#DuHshpvu zaol$8PG2lOm?kH-m( z+P*7zHa;%OYf{P=9fgUQFqmry&1Q{^n($;&vJd_+O|nl;%Ohd>MnrPw+uh@~6F3|M*;Vh|ZT=y>6hn~ZWL7B7 z899gOP7-W595*ZO3L4<2mP)Sj)G(n+ScD3TYB|Sh?xA4;^agfA+(p%wwrV?cDrsW9 z{8fwPpbhmD>CHac4b30?!Hj4(<-PoUk zV&C3s)D%q<4n&7Gxq_F7R(r`N`4p~lk=|-Fw`=Zk6~>~6K=b$c$KjgZnbr1HOep|z zQ+bxcy1-!;xtbR40h_Ubr~1b4X_EKfnM`vKRq0KE$0<04Y$v-7>dP5-uuRH)U988oW??onTVULKw0m*Vz%hxVWG!Lxj&uj9Z>L)cx6T)x zv;7X6Nxyv4bgHjNCwN{2+f^k^COT)Q9VDgWt8OC=njuvWhP-TV$<6qA((nTj42zRp zzXC^p&`G*a+48v3Ir}zJH=;T3HlA;kl!RTJ*tVigIz`!%q(8wUwySl(LRIu+Sq(6k zatSJuG%dD7U#k4`kPPZhr**K4e5D;DB(0l{!!;t$NtcLG!qKIP!a^QK7j$%utlP}= z8}jN&{wa-UwvW<Ot%D)d@qn_4dt+gmdM$ zbYg1vOm*WF=R~U0ef@sCLGY7O^jA9!%8?IVGeS)xB_fvk@zN<~;D{Ici8<2@hn)cX zC}GH+d&s_98}UeK$iDOIOBL}57~8BVh-hdP0DW}_0DVmcHBW}S&xKnw1XfY!C-7q^ zJi-wNu+Hc0iT4?b^O1)#tKv{7!=7jh6y6YuyS)8I$)lf#0gJ<{r>bIt-cpJwjPOSF_mn8UEzeFi2CO{AJs;z=yqPx1(mq$uDUW zJHT27K%@8w+DRWCZajB`k7dOE-Jp3on)5Fl*1yVqjsibY=dW_V?2D1}pKR#A%6$Vz zCktD%zgPQlQnDZdh{1pM%0lT-m6apg1!QngwggszNU;nf5gw;j=avo9Ct-)*>_OCs zX}Nzu%xPg%K35X{qRs4h|(A=~@HdOH;f@**}7;mucAnBr- zb7W7jFwdy_x%-YsYSfo?3$+2K)k4#ArfHLhBNZ0yLhIh`rR7&|Lbf9!8E9SSK!+D} z0Qt1s4$DoOGAVuw=Tj+6jylqYK7Vyh;y8PPr21zTQ8A_cZ0%M(&8DG1>LMf0LYlxv zdN`HZ;O>_Zq}D!`9K`zabPHrg=*Bw}eo~Nuf1ar^8sDx|pj+ftftx zV4LtRiedXejp%t$q(gdqL62R6+DE}Ky^EK~bYhv^lK=ZfQMg1>QBY_h_ zNm^&zplcuAe<+ zjgl>pFRz*rJ2n~U@$_rTnt1n|VvN3|(S(=MY%-(jefFQz>AJ6qB!NqFQSO<^2IExc z-B2bgXr0$5hWcQTZ>skHoZjuFsO=?`6=+L&iL@BkgoE~NgF13^gr$LSMlu0NHH z!vII>ec<_>>fm}A{^*pER37BbWeb|S+CwT@X|ru0jU64!XBL8tW29~nvvp$6EA_CP z5pkEB*1q|NCitph(b8+w1FxeW3hT=>s8}jyDdW#7Q~No%8beki9s`+dr#6Rz7;mEw zqE5cva;|lQ9om5>9A}5`_<|GDOl_ygTr&O39?m8Lj*bTZkfZidvXNVm|B`3jsM1QKM^a!zyly%p z9a#oDJUx5#X3|;#Zq2WrY>!rjp6jwQ_<%h>@7IlQcnZ>e1b?e%8EYKV|Di&SVP~y=AeMH!|~4 zh}?4{u0QVhvl{ckyTQr}ccGzWl2d{YQpn3Jj(9`4jO;${DcZgbj;)&P5VfoSB1+zL zY3q{g<;iqUZJ8>HxGp|*TEv1K6QWFZ;MXYqSfR3D{vRG-Hu~bZ z_Elix%f+@8copWo;E2!p&7r8$Z|)Y#rn$2R4ce#YT z`zs0tRIp~FH|)cO$Ko^Ms+MRU^Go46DPfNcqfGq>`(Rr*LUe2)MP}d0Iinn{~*Sns&+^d8QALItCo zw^+01=qNUD`aPR=foV1_gZe2`g>nk4zX8@!2u30w0){uk;?AUeEhD6B@K3V^1v({` zrxcd)k!lu9rx5xrA|2xe2D70ihg}YB@ixY>gU`U%_p8*_GM?YyC3v_uEOD>xsjs)8 z5i^d#cQ}c9mq*#x;?-k>0!D0>_k;*=F%d0~*^z#^=+|+@cM{^0q|9YEnA*t0FA;6h z;KboIkjWK(f19g9s%lYe{@VBZUoYN&+yDP2PRt`a0o~6Jn`!RiL*zckXHHk`HUs^0 zHek<(Cc@g{Yfh^C*#hq!h)+^l1TK&uVNyD~<_)wLgvh_U_s34sVKI~p-zwr@5xwjc zjlrRvk&EiQI;JXZ&E_;fvuI+6|G26;ZaS+@gM$9Ihgk*Tyj4Q&oc;uSkmO-gCViAO zzp}KM6A+^-J61IpkcK2Nf=X}VR*Ne zZvHE{f5-1dlRbQYe+dade9aL5*L&fA;n@FBZH!Xh`b&!b&sNsSx7r;R|M zjhl&z@#4>*Ww41aM&;P6hOa8Ga2e&)of@^13XH^F@!M!BTbmh%l3z%hzvNwE^k*; zPtw~8b*Q}pvaPU!aAD_VCz?BDgM*Ke9XaxCQU%T(97zY#6HPMSCW0bQ;4#ytA{Q!Waqd{lQ39I>SYNOC^y1>8XTY`D@enp77?--ZEFhrNGYpM&BQA5vvt51 za%#NzWza#!(=g!@CD9-9C~Z8IZ-$haN94(MjY}BjUCeAX-focB+G!9&hW2{pviWnD z=77*0Kv2m&wJ_>w5`da)5IcffWA=SXYT4kG`lm@2BVQf=u)cY_518UG9|H*fN%4N8 zBg*2hTqV~A)0!c&t=fM6mRrREb5Xv5*9{z=5FE$?C`}Bl+3DHmUrqMs{1^ck(EHzm z2i&1UFqUbv)PK!?5496k+n97LU^(I(%1^VJl5S3xehNIf+>4uMgb#7}Mg&XamP#eZ zhCIB$y;b~2h%9)NkKm1Qqzo}#J2An1Yy4gImW9K!kn6;1y!GU!JB-J$F=|HVH|%}D ztO@Ff&XGp2*M4e4v}duaP1pEi;Q@D-JEqt6IC$QNV@5m$$`*~Fb%i0A7T z4b|uZLFp(Kt#UXP_6Qno;=c)xbAk<>4LK9o1{)dYYpd|BMsw>NvjSM)3;!aNgK zOQ2pXdWZuumww|Q+#@ZvQrbjtvrTpNQCM^{{t2P1BDe}=Xh@w@?btVU3 zlzYRUJgd-@P?h6^MV61l=GS56FaAu*5QwdWR;A%Yv--`Y<$mTMlVqk)0HLA*cj&C4 z6i|?r^rA<_Rz1?Fu{caSPh7ogrY-jSFw8reeGHWO4R~(}(c$R(%>m*Ku}lJXpDWIg zZloawxJSJ^I7d8bv=J=p@a`@K=@_TbvX%WcykshY9T-ZhFja~J@~_xW-0Tqq_Mn_= z5S44>CHvrV_9#92pkLRFzF!P&l$t%d&S|V`@P%vIjnF4`kSiu!{94&e6CL&uPls*3 zxrD()Y6k|<9ASlTr~A6(s0H|1VDm?-1sOpD`({;z^5!@0vA&oW^gY*h<{*6JOo22BCvooA~7@nxD-gECsSK+ z-xo8!O#xUzt8QI&W8Koq8}XZqHw=K3TT?hf(uZ2dr&V?1;B-J`rT$~CX`RofGTHZS zn=1hV#a>{V!!5_ByKCn2aqVcb;}4eubT5uKB0rI<RA&=Z-fWTLr(Fy+2EZK(<7GBCHl*VIW8Hu~jV04hw2y~Xt!ks8hQ#r}JMwbqUin&=Ukq&Iu zbR??mPR%@X1!lCSY6bGj>b24x4Xi6>z;L6e(c$TKHat_?_VF>Ri37KxvmJB1!i#GV zo|RT&Z-N%lO?I%dU1i?Lch^O6W@eHOPp`>T60}I+{zw-}>8kD@*%)dHpG~s>qO~G< zZ>{*~nuho}jGq3b=%+2w%8EWDPAhOWR%b~A)oidAg2qu+qBKUDpx*ioBGdlxVIvAS zy1Nb6JdYBS`S2D`sRo-mp*(%5hJNQ2LFVo6lrm$5bxtxhNykl)wpcu@?gt{ysKvjb zq^PDyqinGa=8qSkeK!V3skRf~QU|cw%53YPs&p;5x)k#U=Uj+nH3Bp0Ol|{LG#CyDIgDh1#bT z7|#(NVw|10@Nz+0rjdGFp8NF-O=+0s#e||i@ikje*n@`9_b&;-kMw>tMo=#)CC4>t zHe^qB$^X`}bhM2g#Y&pQzG^b1?E6yx%^TXQbKx=~=BTCJ&WKE%C~!2Ds)w>q`+lbW zBM3S#E=k}w+i@%Oos08MZ1lkGi&1CsK4dgyVwPWg7D^F8ff3NiH~pq)6ZPs{_L!@e z*GUZp&1!mA`YKJsSB=CMTWHyI0s(E^1iQ3l8$~so6-#>Wc|nbr5jeXQ+Eh6mUr!uj{G~kX>w`oDSz#)c=j$n>y>E%mDIGY-C*@{ zwzyC-ng%BgYUC?oi?##k{d!9pt6=exnDl(1ME8JNcL6U zvQREb1;&f@?>rQyF(0-!w=xXt&xB7WRiWE{SkkJCPshyctm`v3G2=Dl*Xa&Ofaozd zZ6BX9inU!i)v{o}B&YR_AoXsh2oB7tM{C5a>Q2=ZB~rzGiIy&93k!6Z0R@Tea!4Bh zqt(IX$6jz#vfc_f1JLfI+|YKhsO$P_L=s@!@tZ{a1TK)=f75~5Wi4za3MlOEK|h@V zjT>urQ!>snUSH72pE%{fV`T%;ZIGPT9MGoQgT1|&H{+J($1yffZ3_6OI_bpCx!}~f zOX)agy&`9!S2-Bo5Wiozj38q0Gep6s!>m$Pyew)PWu)#uZN;cLrU1+aoKdOz zS(TqNy`0WYj7kRmwQQa+EtJ% ziN{%*;aDnb*;-n3kpzm70`UNx&&y6}*%8&w;nqouH5-CCD<%qcjP|5|o$Y+9c2N)^ zlKCdRvn?9trN3~EN=y3syZ~84YkeQc1Awq6JzslTrSLBfMD{FXUR#<#MBy)_K=6=d-+h?NR1V#0ucdm{cYwx zA$1bppQ#R?#T^=seRTer8T1cq%wPkoAJ)b-4zfF*+dkN44EhbZZvNxM`xA8Zo=Rt! zkT);Ru$XuI2YAiy)8HEuBP+xTeLGCpgUaTpbL#k!XoHR;<_!*A&Mqm2u?dc^L{DL+ z%%K$VPgzGIv+hu26t;!~rSaXW+*Bvu+oPO|cx5+9b*!_zEC2QOHQ4Rl+9L*YyuA83 ztyOj9)nk~q{%p`yE%Zm7wbGy_Ugf|hc?rJ$bC3_n;HEluH?!kv0u0U4X8gM~o%xuF zHI=7=WE~kk)7p(KJ0WSe@6GUn%D98Vas?}CFr%Bks2*4mkFTQxm$fZ&$QGPoQ0NbN z3`d$W%S{9mrL9Tf4+b*si4XWCyA@BKGI{Cqtx8=5^HQ^M$OGQ$Dn}DD3KwHy`V4R!o0f{Y zNMZu$9i?#@&@5HkIutXsDz}1=F=eSYhUjonVhQdH;!eWM3iX;S9XCs$6h`9)(d3Ap zg$+O0wjcM}nx~f+--$+Y;#`rdvshnAtO8t^69rggOyrs7=X(@>{(-GSoz#GCM-Pd- z)mq=x)V$K91W1i2R6J81&+nH~^+H-18&^`|7*;WxRV{X@87IzQ`Ej-`e;%NT5otlK z&Dh>AhPX=cs^SRoSZS^ooJnD`=XytHc4N`DYEv46tKH|3M=@gRu7$)W zq;BY5;`Vl~EFAHc@g+KSr-|T85wpLNHFDRc}$|6Yxf$4fA8XvX!WnU2$nQ6Iil;ku|J?izQTAXO6E{5+U~<|eJrk1Nu$cTc>h%{FjVPJ4-vbt@nl6db1Y zMy;>I1>WkAXxc7)^Pi0(nl{2Fr|1Kmns*~z^JVs>VM*G>h-i(_yLVvrATA4;#`R9(lUo+>Hnz`;K z@()QrCC(@1kJ=xw_2&>A*u98~ICtjFMA~a6a_0*fxU)`#m?jxUcKEpG29X8cYJm>Y z#PYfw5T&*AmP{F1xUk-2iEU>DSo}yxOOTMlq?G z*1(w4CkP%9SSJrI>F$CVIDWjbKm?M7Bw8*S5;v=$dT`;><6e zxN;U~yu?gGpr@p%>--)JYb!jkFP&$qJq#Uu*OekI>PAUixum_%Wgy1YEuUQ=yupDw zRes}>moXAkZ7s$v6(mo+ayb-W&z+|=?Se|aRlZt{x(@Cm7dYg|_bM4|qbur41=_!f zk0>)yJ4bFxQ}%;5okjE5(3muJwIxX z^Lqm*ICh8W$pm+Il2&Xlb=@79XCzr+nOf{EbrA2?d)!3^W56BuAkFIssH>)(r_mi4 z=r1Th^;!&6QFVn_Z3&e0@RY|xj(XTtC1pdjuR#($prLp_Jx%VPS@;)i-B)n?9_lN8 zu3ztex@>;+kOZ9V{^1N_BR?TKK#!OyBP1mFnmg-w@XaEQ-VISPXi@1~Nd9J)KA2iq z!0k>C`*&h^Zy@}j)j&xc{^W}_%&YTizMgG9AZ`7_zTxf&JW_ag9313V6&=e2WYS$x zUa2^Vgm9M;oqErrsOlQ1g`FDi7|2JNn_S2Od2E-B-*uE%CYfc+)#AozV~Q;|$&F`X zI!Mv|g`*-%7;#crP&7G%^|X%FVwIj}Uvf50^ESi!8pSfz(mvbCPrLf3Mi4%i$9f3; z&n+_@?gjDbvoME9(H@o=e*W$aeP|r32OB1hKZjth(d=@E)~G7cqp97!;W^NqK{8}_ z6@Ty?n)21`8UZ<*k5+Soi0W)VWB>aKs*iFhgMF=@*4O)AVM>87dzG&)+&`$haxzi_ z{D?jY#Zh4&^_EC-cpK1L+kiZPC$wkj@A)rxWCyAGIA@~g2JXTx3#-cfcVQ$)0|NkWa&*X@LApLuH+JW>(b<|L%H#kAR5jY+lUMba)pd1**h5fxI) zGAji$-C~vM$Sm965!M3(7T7Y4kY<0@5MLi^Mhhi|d^#1vNSH>&E%U-BAf9vm5_IjtPg6?fp#@8&^N1{d}BacH_(N{NESw z)k03+`fKr2{;xLXKXBf^ogYC86SP7Ks5%{*Rqf;|S87&@>8;3-pSq*yI_;|q7`Kd#Q!42XDg)kD4 z#SF>A>;&i%1;X-4q>7x}+iS$Lw0UMiA#s4bs_^g0WoIq(DtjjnX|rv`f+j0aN+vTa zE^OgKEwsuzlymk7#)~^VZJcT(#BBFr#P=SQiCzv}q*HOFV3Dxy>t-T-?VEo75?;lG z{nlJ%C6&yCtL^tD1N9>+uI7;YexoS$A)Kt;o2oLG{c_k6_)PoE=2ozlRth5OqY7us=L4WJ%X*`kyapoo^k~^%R&9mwB z*@y?5RkzHmm#gA*VwH`5Ygd^Cq|ibguk{fQsUOCayu6B;xRKm~__0Je_&d8gw5uz# zaieKv9tP~(nyiOU^p`mzzM)nf49rGso}59mX(c92-U;9&Z?SQ&C44a78Az-OV#{i- zM);s<7LDom1O*&R8uj-#uv-&^cw;4?700H;0iR?cAHVt8e1o@*5QLAz=8dzG`^J^w zOg+2iUSFT}2oB{B8f7Z0k%$xKd3SuktEsJxn{J(wP0FQ8mT|cDtr~RQ0^z!1vUPf4 z`ru0F_r2F_;hdA$RbOjC_F5yZ_?+x~bpN1(pzO^H#jt-i7ErbTSuQuUl>5n8JeCi{ z*27Pf_hsU!%mGa41dpj4-7}w2gz)4>Uzq_vY*L4TSLY}!;@2ij6r;6bxAYh-EBz|<-?N2AI^*q z&yV-gc-dF&6)TNNS(!sfaeXpf`m!rZ7SK_f1$$z>$3tecEqawe;kUuCl(isRe4sZ& zlr+wnmi^$l7O_!AVJ1P6aC1q$$Yn{nLNPjGeT_2Ad(g;hWMyUj2ot#@HU0q59av!x zz)l@uQy-Sm?)!2Qy>h?vGWG41dq(QnGppQB(b1}lgY1~or;4UT6Z*T|c{I4frtnve zV|?ZKe`r(uGsopzod4z@_N9<6i}H0WmoUk5qEUQTNzK<59?wMseh>f&SBodqg;Zd+ zEGvknz@=7Nbnh8CM%YOJM#=W32bS@^27O2O6VN`A){?g}OkeZL=`vf^^txKO;_LAR zoelT^hjk7@{7E9y!psGit^@8$L=eNnxQ-%hhv)+W(4|dk1{);a*Ya2~(k^l|@{s$U z-kFvzMS{cDw=ccTb#`#Rcn|D94or6D9sm;N{-QQ+)BGp!V{$aC zq=adD8gI*)lqPL@*Ru=xjTTnw+G@p| z$yN1x;TM5Ul6b!|W2WfBC>j;#Lu%QUBb=Ua6?+&waG*5Vl*qDw)n6U)t)CtV#;9R3 z)f`~{C&m;kJ)DTN&g{sSZW%p-$f#)e8`hsXnzh4XM?mB4I7X)y7KkEu!H>lnZmry! zKz+NWMi=g)w3TD*%1b)LIZ2)QIr6II)b>3EEb$!Q*&j15iO&55Fk$RLfmkb(oVz=069qh?dLz z^i|MnTg8`&leKzJ8cBFK@1%@F;csUCGB<#1jwGi_wa!pA;Zhv%NW%K-MGqbyW>go9dyyHQg z#vbW_f_|MIRzjagTa)1qiY_C)m+w!((w#O)f`V3@;6Bl7f`XgMU_aEW!ji>8ZW6^Q>(*kpE8JRp2{=ie@qhZbLO$O;Uu^e9r+j07GIX*-_S@??%T39|C1p zhYj&KV=5`UKwu4IzrwG@7Qu$+u)FOc*c z2R7xN1R#Q#Hif*(%~ejlD6Y+nS*U3+Jk(;cP)x7BEk z0t3B*G{&2&3Ua%_&ol~9dND~xuyJMyCm zwM++&Ha{oi+@MqrS!P@Wk-fizA^N9L#0U0ZO(t_*idsT}B&j``7)L`sA&yY65%#M+ zH+bvhxM@>A-nOXHFZgt-4)Q9`C66e3#`69J_B@N!$osR-FeUaWfQ>u&46C5bxmcDr z>>_X!XJ{VAgJooTeaqZ@=w%Yt4r+ZZi@hZ}dy|wK22l(UZ}0P=5s+A9&OjQ*a}LJy zCcI|YR-!_y(<6Mm6OmsYiPD%g0Z+W`M(_($aWFgxw98sYObc;N_<>_t=e-wgNyG&@c6k#Fc+OytD zRo5y-{v8hF#opnf;N#W~S+Bms=cEle36JRahbhkUY8{?W=Ho}qRRW4W5(`+1*k$H7 zkWm&}W>MworA033gl<%pUM|Z>G%OSCgv~k|;#AEDZoWS{puA<0pGI%7>))gQXm%et z`R&mDBCEiE;X5XqY9#Z#rR@n*XZ2TO*?#%^eSdR0j2QS;faQN3H2?jdUfROe)?(WpYyv|J>Yr?@# zKuy0kfY4tdZOclt!IT-xGs#nS(F+>8icG>mN`LQ!;7x95_5O1f(?=Ne59>VeX0RjO$Mv#4Q$*I{DLHL_Ey(CM=m$zkaOe?%0=VGV!vPVTgy>tsau* zgpa8EAq?T|XDE3A!m-{P{DBD}t5fKu9s~~f$ig?P_~@Ho-?)iSpo^;ORrMc`M6WTF z-1lI{;)uu1Q*jAYJy#j_A|@IKP3-Xsgz;f?)ox65A&t9DWgkm65&;?Gwc`Erjw9bA({0`yJs_L{)9*p$$rseF^{XVPpeIxYF-7>~GhLYXkj|o` zZai#|LD!-+#IZ_0^o8C%tZfk!PrzHr`1+6U?8c7(dvypEZH`e=iSq0{>yl&+@?k6c_&`OYeW& zX8vb(mx!%{%YU;taY>W%X#deQW@$6G59|vhroK25wtfCZFFs-j0WD-e-UaIJQ-|i_r6)>=Blb|h z<8`}ttAwnNbsJB zaq}P5CHQK8=^Sa-JIJw2XwWX`w|Ri-XvMc45MF(G)8oB5-|IQ^siAed45XmSAAMJy z600C5ZRrH%aUWbO*xR?9u=QbI8Vo7&{i7zFTmX^l&a9^jG zI2?WzUKbtN0Lx1>xE}tbM}6BT!YpvPp~#&AO!O}* zw>=G0)lh1<6+8}&WxXe)4`b#GrLjul&`nZ(p; z`?&&>F|@piY;;vVo478sYHF#eB=b$Nu6711Uujn#S$P zUMLXFAOKZAjUS2Oh1pg(AI)RktsU*aEr1r=H$o4+#PHq@Pfl21gR`%Sld8c`!Gqt9^8?s~N7j z*jxq3Xpf%&%M&UGaF`GwD>B~4ivhr-xEfz{UIb!T4%AQ>K% zH!csG4X@vd`%X8b9HPV8R@Eq%o>l2q0X$KepZ9k9!RU%|3Zwt4<5)35oP|#O*8&## zk_X~oqimyFhA@g7s%A6KZI?sJZY?n7onVU%>MAF^0@4HGv2*VO{>yCZd8Bg>#`fd* zjx&*5F9D<_irdhMoM`!OW@<5e-lK5O#!2pr{2kP#@6aiHruh@R@e$9E+!I8U>ZT9r zarl~rrNg{ELXVh)?;(q;_d|4z38(ZOWiso-;)!9~+_3rFAPncsm0{xa{?FZtj3+ww z;H2bF#FJHSar0t}j3U3qiHOSFm}y6n)P`@|nvd(>K0v3ruMnnH)Wsa+MT`k!({iJs5=H`&8C)aUoEU_}Q-)f~gz?;6*z z2jF*XeR}}uGHNT{2*;Va&Mo5ncZ4SvfL)8`qBP47^2s{mIj;O7S!YKIE}~m;1L|vI zK8st!I5jI7(~0>|DRzx52(=}@;-^=E@P8lu!d|G)G9cZg%G+>hxg zQrPYbrcRgR`Ak?OO^F2(Oz=K2MI@1>O}1bPOiwl};$G2Er4HtdNw^p9&a>yv6_ENE z7M6&#WSEV0+5Ld!fGGnk$&fs!+dNSQb$h2?`W-cNl%*Z#xgupavQd%lu%C$+I!!n9 z?FaSj3K&vT?Vrvatka&S&|BQ`xE2b3&U9_?fe!(K@}!l1k4~k?x`?k)3a76VZTxX#anI@Bb{~ zvho6!LXpKgd}u{3U~=a|KFjE(KInpzzj_l41ol8R+Sol_40umxnDg0ch+)wmM~ugf zZ#47*(F6X@rNl_+^cSQxPA@&oXRB|0o0T_Uq{n$J2GAbD23hdHnt)*MGGy1GYSIMt z$t9bRU26*EKi!87Jea=5a`Ir-s+lFU*zw^W@L}Y~tL@deAP#kuoJ1Ks(={JzQNUa5 z=%K8si&Jx48nPKg=AwACF*(pern{{%g0AZPK)Aztj?-c^ig=_c{FXfKj*^9JJkHv` z*iL73K=>A0O}16EvzxoIP3k0h{1)r1&=hdku@8FP3B$U zOeJv0OE(jsZD-l{EKK=Kixtz-F4vPryiRWqh(p%JQEbaIrlwReCk<;E8l6ucZ9>DX zKdO?G2-d%3-7*yEro>xn2}_%r)3Byg+2%)JS~c^%KRA!eZ9z(cl^VKZRGrNHgSMfE zmFkN}v79zCV8FOfYPuO3%IcsVGfuU{4b?OeDMO}yds-vQPRB1fHd>XLhzu0%jhiff zqlMk*&TG!yFpC8jdsev3P@;!O`Mh@jLzi~%bFpQ~(6sVef24$4miSyJJwQd?QV}IT zI_8^v9xXq!r8qq)jYo`ShBT5%FO3vidrFqT1ap3@p~%8k7_+)(Wcv^{v`CI7)xjQQ zm72fV;qw6#&&gNTm7=2|T~N|=3%*x3%sKCTYOK3Gaw9XK2X8yfS~1FrI7uh%-=-}) zP#HUGN9U;`vnYq<=8p@JeLDZqK5+c24$!-Qn}BiQm#m3#sD0{VwwG|t4c^+M%Ka@r zV82*N8YRFlumGz+(BSru+qHbDjo$^p!}qSn&Afw6u3?8*C3!$xJ|KSg$*C# zrHGM>kt2|`aOtvI;Tl-DqOzS5sa?&S0s9UWXM`%zR_+4W$QX5}vGgHj(osd%-pFz; zk1FIv-602l^Zujdz9NgLFg)~i>4^YmBb5uH8byQ~ZArEONS0WPSC!keNc{!$ILlVN z^!#!RDDi0mq%BTOW6GRQuY+kED8%eMzT{?h8D@bsP1ANFfM17CEXC%AqbvZRXIa#( zuFBv8HlCy&R({(^M^Vx#2d}>XEA@ylVEVJ+v>uESaG;|%uL)fPh`hCJECD;^^%(Z9 z9m*7wY1vwJOD@i3ttm8r_-b=bFJ)8${deVi?JLw8{^}Z>vz6{7#}*J}7ojixbSH=L zAIdV;9f1nU9$g*^iu0JNE((>OXgA81iniEV{tc-?k)P_GtF;`8*Nb#TyYHNimFAg!A@dQDxkg)`!yDUwlHgO;A)BNzMoX|a)|U;dA(VR8ni96h2Zt7^z#YPbgdTP z!n-+ARuij-zmV1{6d@`|6fY2pmlH^8(_sQ6zeR^MPgQR0UPr;+;UVVWCxaLL$vyf34Z}^-8jBng=5ueP6SMNI=Bcw4(MONqVu5fita4T*zEd$bl(wO?#oThft6&ca1AIXurh-Y!9B+hDXYdb!dB z9rn_dik=1*USh#>S@JDg^-2o#(zO5rnp(8m{k+;zNNORbJ#2-4Jvv0$jiPqY^7a2P zW&e+EJL0QOn%YkjjQi)u{a=Qv|3`hQq$p};;_wqM{lj&b8T~IfCQMbw0ap#_E8YWt zQ-@-m+)c{nSiFN%zAA@3R3>kZ+%7sq3Km+(iYDn#Gm(t%pngW6;6Gr}x48(Id_ifs zYIdlOV0Bh8@FHp6Urt^U!j<)sACLncD>;ebl73k%EEY#O*Hcqhv)@~D6o zLBR0Sp45TlymZ4X%DmM8B$PXM#S$qysM%LXP4D=zE+ay5Ury>hDO|LQ2ymPUF%pw> zaM5J(feD?bL_r zHGwFXL`q5J=^U8go#y~8l5g*Bd5ads3GAoF;aIJfTH*Yv-~@DRV7qOtCqQN(gSR(l zR;s6TC$ofBW(u=Sa(IJgQ}<6Uh3KgF$VrlZKL06Q_TfS%i!>~1nsK9I(=>OR3JCCB zW7FfY4QiM)l}Wtz!dm0$@H?qX(9MRAg8Ib06-jdgU|Z;-uM(~3m#ZyeF?g*|DDo}9 z6Y25_2GGmT8X^s=(iVs7M2vbL6P~o1MEwTc$F>rJ-eDk@C;Oss!#dc^svKG z#(oerkGNc#+`)QZePCSamee^~cPy~`=--gctH+)2WA@a{XP2d0q>ho+vR-22=vW=}n995?hr33B(U7E$#c%O^IP zK@7+%UB5Z6v=^bRI)Yf>boD=JYB=^8>fxhV=tMH=|w%k{d;JT>%t!U zsckW>7>jMxUQB49mWaJg(6_IK6}v>Ool`b7R*WMr9wpU(qjYdmDJ)M{>O^a-_J*nB zda#r^fD5J9-p+g6!uk)hw^>~`fD6L#$GX5#thHyX=SuO$Qhkl+o55CixrDFSFfxW{ zl`=_Wm9;R5E7D7AvQ+NNJ~VlIB&*Lp2I%`LRMOW2`=Egu|lw4+7vdcTi9|> zm!GRsASpv?xp2Pa?#}jnQ!7czo)?!!NN`3xl`7v3 zP?(njqa1DX%#pI!*#ifG%{6PQbn3!593Np~N(2@|J+@CumJkkyAw++Cqv)M0f7*2H z8xhez{{yQME>SW{9BEAPh>rSsO6k((lH|ftru<-ACYIKVR8y_0*m3!}%zc<1i+!tn zZQTjM%dXRk%jc3 z;K5>=c*5}3BAS$g-u=GV^dcru)pIxlE^DX*(yU<(`1t<0Xt&n==1I2jMlpp#>~<+T zD70#tcdF24bbPXy@Us*yWl}1cpN>thq^QL3v2uh{J~7MaX~ul6MS@yNaA-UyM!dMC;QCK+}q)9PHJegRo#fc%djsV4?|r-3gh%UL{UTs!m*X!afB zEQZGHqAo9xUcN0(;fvZBx7Na^saQI~-#pR3$QvClzDKo-It zl{#*si6l&B7)#BZ3*5R_`SW78dVvrmJ!2ZC6JEi;-?IQ({@E;so9mbtoL4(vXor2V zts$8gBK?tsTQlF#$rrY|J$`+oWqHG z%71*Umd<3Q2@Avc7%o3}?5aZ&)W1M**P1nL_cR;v(s*HpS?|qmvhvL2mvDP{f+i%Y zk8tEV^p)AnR3K72AY=(?9~aLEcxBU?{st+vk-p9E-60){|R$|!IXHceCXZBr4xP1jK ze-TV%5yTN1(O==@742i!%_z%lma~@W$|+fZxExY<+n?o`F_c8NA)8lqYRAYex7_{2 zuoc{_6)c?k_`(K=M+od~&C0+s0>@4}z zrgp7SX*}Z`q1)9`HmcR_v_!>d0{1zL?JivJ&S$>UHt({$5t?9r^>JJ#kiD*-XXEUZ zSq6{_n6Y|Cylel##jd=(vY}1HjbH7kwmgob>S38Wf96n`^8Y9y#lxA+RuEL=cN@Ov z#f-jA!_jdnNO+Te_Z-) zH}&sf1=fb)x#7<&ZsVh8%RFKfR>pufG^qKw~hkR9UmjUzK)CHgNFL54i)Ks=Zvkib1Mue6#y%@HC-j zZ{=$2C`|`Fa+mR9SaVRMBcz9ZW zTi@aDy*jysJ<03w+gRz6?A?)raHY^HFCYo=M%1b^F_pX~qj}(W4!$Sv_1Z{QbIKOH>O^$V?8@jK-8ORbrAYEnl`M9%wl{_u;-fMOinJ5sk& zA^FieQ@2#*(AY29cCLUlb;U&MaSW<1UtzeB1mG3U{{NzLKc=7YF% zOnkDAo65D^RGNlTnlHZ2E5>JXau507`xof{-IqoDE&0v!qsXNG7|s0m+P{dhtoVPg z{i6z@@hxs&n&=e<#zVeu6p-d?*3;)VX76x0-r=#B;d6O@PpGd3dSj{_>?^`*Ez-#5&m$-4@yCyF619gc z<@jYHQRYD#YzsCD-cqc0djviN=C_HPH#Of&gG7a%kDjM2^ox~;8i~MJI-Y~JfhNtz zR^Sgyd`?!4x5Dq8R1NwynGik}kh9FLK2SNX{4Q<1Y=+c|%U5C&IxY?eOpA*ql4l{s zM*=SNpex-Sy$RLdWmCzQXe??8<`?4jzOdX{?KO7q@7tzdH5Z;JENnzkQ0%1z0r?EP zw|H%;QF~xA%FQLrm;i32dS@^+52vJ7P0GgN{dKbhbbiewWHPezVv$$lxgP$1>XWYn z?IgI20Tt^M>)4jV?x66NtaG95K_QaZoKd1+6>zSTZ&;)EPmoL)VUPvxU9@JJEcC z55V||k0}d?c+F};*|pG#0yWKX>T$;-U2?iJE~pDcwJypLKzw}Zz@FaVctFERV#nVy)6^HL*+kjtmp_NVMV*{b z9c}SB#rM($0=v%o28s=hsDvcc<=m&>bF9@_{v7J8S*hSAI$tE=SI8|d$+{JVS1$E0 zEp^Z?pWMe#E`G6PZG36jjcZRl3Zq{8(^F*mH|yuYMW@jzc#ji?PU0ogMD9^Tnj|N9 z!1J}o>u(jBs&Ix_J^7)w{Zu7e;c;D#n&=K1tm#(lNJVx zCvJsF51m=?uRH$UFufVRYvR^zT0&oH{fn{A=wFsi8Hka~oPs(MD#buCzEu%;lM?)x zCH~}w4MBKj9#w`XJ|9UC*?%17d8E6I-r{jqD+Gwii^34`pNt?Z>ApK3{u8nQgFu#s z_Ok&Oe)roC=^N~YjegJVckpipop^Q}bGF$Srcr0!JK{kG4H$IRzU-s`K4 z2caNpr`cf4wI;_P=t!|i!diNHc_+>Yb|}A#xCv#f@?By|wAGKz1sC76B4%7pmE#7}0F|0TIDQTs#Ta=7foq5m*B@pPgdPh|1d*&>03&4; zPP0+;T%1-ufrF7DDBM+M%3ed(nNE3DaY$B_sCM(|6i530MsaP9xVA7V0C}--U%PiE zhwiXFgax{7k@a$8EudiS2J<+Uc-bi&Uhi6Z|C#clQaH zo94R#Iu`Z!oU%fz`R5-EZ`$dbTdpRJW)tu_Oe=uHFEQSDCn)eARmE5--FMPTr{qO< zxoPQNdq?{e(v8I_4_?mdzWZt)l?+8YbBW>D0qcSF!hAUD2eaIx(#d#I5yr;`2rDf; zqZj?1ltYtPs6C4NJP!lX7BFlBT|2*daP&AvpImFql{?S8Ye}f$s}ZNP5`-x)4t5e# zt`PVoGL?@QHp-t1gsZnTjVxLhgo!exmV{2!vlr|)r{PB zcSQvZ$YV>m%V89!f)>c|>YPdr*KFAC|NCeAkLk7@Vy`w893Y?*4j>@1|F&fQUyy;B ztBK|RW(vx*VSN{u9|ew&Zr{m1AJL(ZU~mqX%dMh+)ffIkn={s#TQL@LB9hyb%t<8; z`@y}2r=K?)i&cg?-UPvylxtu(Akgx46dR**)YqlLmZ|fr_O+<%lx}U&d&`UkhyQG$sy1l# zCkE5GX9m^6v{qrM-{PUKR;}4bWUciVa8Zk2G(eT`mZu1uUos+_xNC+ufunA5==9U` z(2l_Cu8)=3)7$OOcEIG1mdt3}Y`RzWUppRvQ|ntsV0_R&>zYQq-x{NH(0D>`-uuA# z@5X2Bn{9b<#TfMK^d?tpI6YdU*DKvVW(qF7JR!d{`+2+$70@pJ{Q#Zx=-2AE1nAdlOuG=rfFG%( zdGY#_n!nJPsNhl_-Q%`lTh~i*I6~D|bC{o}W|YrsEy3sh1{rT}wgiSh9WvwK0OqQB zFs7$#Kj95|MarO#f!@actE}x4PV7RgW5-0^sLJz_}53a zKrdi6=Hs<~vD%;rMP5U+I%zshhGNF-E+ig%f<6nzN1>`s9>I z!?Hs(>4tEWBT^BoogpvUMRXWy4mr3;<4`G!xqOa}SeT;>CzfO=Qq5=t31G+ZXxQ5aXkO3g)0LhILgu%6?*M}j;6R# zRIiX^2sNzJt5<~863QdASTBlfyI;|Q7%y&)vDA-y5yRFpMvS9rxpEdS{*G1LgHv0p zhu5!C@;udKk!RX&o>R{5iZstM&H64=o*DDfSJfSM0oh|jn9eABaRVK3d3AM;-O@!h z4)<6oceq%ar@*7Ig9}4p=;TFtc}bX7U#=i*32Q-55#}^ID*cZuE#T;PtD+-*Jz1^f z#)wY(kR?h#&YB7j$>;gsC=F8=7&PLon@``+L=?N>ZZ4Xs*vqURvEk{ zvMBpHkX63h@Y`Y%_T zx}U4`Hyu(u5>(v^B*nc9*|S>tKMa11VAiOi#zm4+7^!e(yk(pW>a$Hb?U@zi zLfQcqlBwykJ0!mF!wxUARC$^vPG>h$P$P``9i&Jl*>V~a>lj7)>5`Q*C@xRZv8W^f zhj54BK8}rb(grfIglV|qi-lCEDxEaXPPSt%;%M&Lk`yz^0$No(sc9Jg1VfQllGL4?=#A zSeJ!e&SJlWU?#U#lWKd}6AwYW7EB*UloY1DO0k(5qNgPcrH@hUia4iYCgZ~zH4Rms zq$boY4nVMuOBHhHq!tyg?IP=7#RiV0@oS;$5^GXsda0!=mhvP;YktX+Q1bDw*)uZx__ferz|q0We5DCh`tpW_Uza2RojO7T0V;)(IxEqL zU01`58!cr+{MV{~Y&nY{x&T7p-^BofTN3BSq6vmGYb(ICf_}H@{3%5Ti|bvN3fJ`c z5u=mM3&#gfSm^3>uAsUAxk34FQc`=Z6|Sg@579w|TNZGE)f>VWaBvZJUs9D9CLj+vU zL3QNxQu?ejY0Q#O=xT`T{`bQ~E%Ibx`;Jv3)Dv~eG25~~&Zol1Npj%PiFh&bV@-t+ z4*i=@5`^J`i+i*Wm_;7}f{t>O!gBJdQcdH()aL7+j>eWkH*d)9MD83GI>+)NrJt93 zRutq0WC}jKu6`!Ua)iVAR3EVsDxq^4Y;GK05+L3U1?*G~t=hFLQ<=jr(y-JZ5SMk} z*`@EJedW+1h3V0)iZwZ3R5Ag=*jZX4qIauQ5+i(UOv15Cp{P#_{ijG52&3CcmIHZ_ zZo{ZW=5nzpC2X`3_2eUreQP?4YlZ-%mibVbSh8yP9&M^(QPhJWPk8>YSvIFsPHj~C zkJxGhOXFayI7TL#NYbiO1R@Ccf8{!+)8={{&yL^6CY<5Iv~G@v(;={=3f+AIhuyHI zEv1pE;VDkh8U-%joiO$ z$SUYczoVD|MWyL3g^kkInjM+Hq3bIZLlscdg73zdB4!V9Hs<8p(ncgFw}U%h(A^Mh-p_0 zuNWyyVV>tE_UVc!z`{GkGU@VoJH`L0%T^;;0O8ZFvf=EAv$DZ)K~zRsjUj`nzS^L6 zWOiAr3n*`X2p~nh{pW)J5CryQgt~`vC}*5nN}f-7{hX3v{4Z?Ml_8W!^RjI{I4dpyz}*ToL)7XaYm`}O0t&pNA{!I{&@EhTwzIVCz6oVrBnz8U zx~7O{P8!34)2*#|)5G|)fLt!!UoopY#z`{Ns{V_!u3)>jY2Hi2g%cw-Jrr%wOV|*h z305m&X&Kh?T{>G?zG{%6ais3kEw`K3-?TP1pBrSWXK+8>ke@JwQRQhhVSZllS|UYXtudMX3Jpsc5qQF*9q(C zTrhHjDFBHWE50cbH1=)bQvITCVQnVQO6I3^;MQvExrpR2PRw~YRDqY|f~|u+@GHtC z?a0}KZf0tFuhx}~tb&vQODX#jP+9tLzH>WrHKgkM6oMqswq&>BHp8^!(#8D{-Tj%UffbF@C~>MP92 zH2HCwSiKv&SSxUNnPgAh@g^C?xDB%s7T%4Cz0>pt#V41r(ZYzWlFfp|Da_?`&Q$8y zJ#oIAOX8JUGxJu?8_a7f6}nyPHN2OzQ@KDJCB@xe=$U33@t7R4z+1b1ZdOE9HqU};Sz!BxmoEcl|gsqh1O4-BV)w%xmfX^f$M?%W_IG-NUV z4sTFr+-L*jwo}nVSRB6BRm_x(MWPIEF)IsL<&yee<%pn<##lS8xmR9F=G;70L8|k2 zKhb;h&WVr-@_XJ0bn+EZkoyewc!_Kl|6H17^#yt31e})mAxX+o$yj7Xd|1& zIjpd%XLA5C-T)yr9478xDLhv3Z(QZ!-@!*@JN0=%foDGPhE^e;{x(k3h_D*)$cnFX ziuP|dEQb_!cT_E+QD{~UL?vqMq6b*2z~5$c$bcoJPdvPU3v11t`3ADFv;duj5- zDwpw|A5c>(MXfqmFd=u<7u2%(97B>@gXdGA8zwp!+}x^sFA!`$T+E&lwN)n)FT_Vc z?%In}1gRy+q-gez(BM}GZYn`$Gwu>_1KTBV7fC%LE8?fi;bKK8A`D)bYUN%!@f+u~ zpW+r|JU^C_6I4xnlG~0*A_tH>4)5<3nB zG-O2j?VNGXZo&1<3#^xCImEw+)jl6Acxq>xy~(A*WHwHyuLnTgKugL(ZuT0)O0rhA zjN1mU{Ir!5I_LCEw}0kQ+ta}C7l=kbe?ZXUUv&0P7=*r90<2@|HAI%TJA<`e>sh))@ljGUa-UB10zs}635vTDbI zNpGlJUK@cRO-j#5;i86Q;HA zya9bDI0wMjj?W)@d4s$KTriNpM0h1i@c}j}#2_Eh<;1=fLSqP77y8}+O3?S%fXmSr z;t9_isAx73M&hr|^ ziI88G$4hZEz%cCllBNkW8-sph;{@>`o(tcLUVFnc56lpy?u z?=_MvS%_J(NiN$(QO_=t(}nCM;exF})fkky@!_tQ5e4}YbcWHTUKu0=n1OV>4^!R1 zjIWO=j}BC`uE_fkzy}m76Lgb_57;bKCn2Ks7g*WyuN$G|0bom)aZBT~ShJ{#?&a{g z0jk=IQ;ug;x9Uhkv};)d2Gv8Gy;6%_u0@Bfm8P~a(j%>vr4BZZL_FV7LIzMC^TIff z7j9Yz!ha_hASvFK^&NR?`SwmC!u(}^%i#u!9e$|!FS{T`>wz7`tL84x4cSvGFl(nyn+U`R7cQ zKR*avs&kH%Jqc~=DbB%`d_Hycoi$zGA3H_zq^nF_xm4kq=4gh}cmgL`E|lNW<`qXjK02EA4aqi+-<-xp^2I`Coib!b9i8X@b34>y2I^wDvm z*bgc-Mh(HC(pAoQ@@A8T&bQiR|b0Q=`nL>sd__>EzpcKUZhEonMNf{kjh+` zm}?pDwvM6r6so1K#h~4wK%Cf=5+3^8`~@&EEb8($g(Hm0OST!)*!fxrQTjvV9SiG_PxW6M{;#}fGu+KR&$;|xG!7*5=$fUhM)O`FM?E~ z9V{<|h-2(U1WPy=9K9?F4fsNGBC=1x!10JLvbBj!JZTKuGV)UWT@D|;OGg`_I^v9W z-+LbpvooDmiTsRzF8@qso(x_nr3}M_OB<5#r%1k9K62|3k+yG(NxG>{h{$MYKg+BZ|Z{PheVr)CCfEY0?GCH$?&r1?T$u9~?U@d=Ixi@zZLl7 zdtdZ`$a*8hzcD(uCf(rQVFD; zf*qocJeLphFBBg!HzxG<8Jwkabqp!8qQlii>cq|P0_>U*#J}NJiuP<06Ke)1kaLGX zshx&OXvwrBN|N$xhxGF^ybQxLDYb&hKu^Erc%^nG9g!a$&DSoAL_5)^y4Ft5Cg3af zGM&H{>G?wsAd4CX#56LG_@N$!mBL<`EUlQiw9l*Znd<25a>KBL)B#OnPDapzu!}$^ zc%LJ!b%Yyk6eKrN=T~!GB&XXU8Il6`Ovd>z{IabVCAtfUDp6tHx5$|x3VkTPC^I7k zO7i?r_?}SvFKD_6vwifw>}d*PAXKH>dBPjp5DO@%;ok!prt4Kh6}`!lKLdIxP&z9! z-!g$g&jl)o7Q0~JimnW<#7o|Y1M@qh4q=V@iw~d;e<7p!R5uXM--%X4Fs{Syw3Fpa zAx$!z2ImHA>jowp)f=zRho7YKCL`H~;3r?kB7h9Y;SIu=KUmBbJc>XJAPN5hFW$;k z4!2ohUiY280J8!js=7@eIkFR3OzE7!o`g}S+0f~RTr(X=BQH-#nS^qWWwbe_yI!uh zIz6&5<}A~qig<6vttdK)yuvmrL*kq&DfwEtxOFUCtM=!T+$cXF=?BlTQ+G z7Idn=J({LVsPO!iL-oYpZNw5`_$BGCyN0-H=Z!>v0#5U1i6iMnY|FoK$3MxoHxko` z&@GG385|Kh{r-Vwn&(5kW8j7LA4f$IQLWX65{=kMS(L6Y@lGn3-Hp!$5()3_K7BVdB#}}ttoMz7z6C`*=2F-#rst# z#9nFLMNo|U_?`OJ_=;dFNT)yAqi&y$ezh5?9Yu)t7a=*B2_J!)Efv9n{xXCgj9gPK zA-Q|<+ylVH5@iqX6v21Eov}C98tup&gMYw!Fz@OC)|*lYH-uof27*;cd#LJ~zf?8p zSfL_)p<;yFQ|bbJhM}+uWu{C9q%;PUTx(0J;~eMR9BK7VrT1t6Ld}#CYM%4-)Q-6~ zMpczVS;jasn4|kq#@s8>*qRLSg9uEp9Fgm5bVvJM0|}UsUN3r$KpZisHz-PB8j-{8 zVzmA5L(0B_xwP9pkn=;Mdsr`llAUR}-k8?Ol=OjiqU%|db>Y*ZTLq*Ze^Ea~cfaLZ zJ>lcik!T|e(@^s&XmHa9U}(i5lUlPx-zAV5+mz{I<%^%qVS?f~vw!6co@{c4SN`pj zvHU!Cq>Vtjk`6~olRk-#^8}aezx6~Lw+m|YWW5vXoB0k1ybXP#Kb@Rg;G@m-PVR46 z9@`3E<+8sD(eXBP;wpP2T=sS&aM5cQ{O z^Pg>I1S(k}eTXvg*IQZ{4>T_B0yfJv zDWh4y*SP)(dPCE5b+0$o-z#BmKzffO$LJ*A!|tzng5gr_Tu}B#glI9ISY&)gG11a+ zdp~^XU4^lnD>&LR8R+ER)RG8!^+f%DCu|Sd#6gLb??78JXBQ_C?@xoD-6#n9XhY)C zlIX5!^C=>E-ETmgFC#4}?8e1O<_o*UA{++jqHkg$buS6hwcyKaHKnkKlUygpL)NN?Rmg=SerTqfCJS)?liYloa&uA?cN zk_?MBbY>{PSsWifQ|arN$iP`r$;s*lcFVjE`5hA=mF?KR6k+wo8K z`|Y^m9f*C;7>duRW@w)AY+gsGR$+(X<&IH}B=-^0CFGQ3H5 zqhH5qp@)a1H(lU*R~#X5Be%q!E3DWz@vXVw#}y!SkRMU0$nkN;rf~D7*&SIR@r7;W zp`utVGV%&J|5uuWc;s^MipO{PvbHk9|8}MNH>NW$)$f-8yRAk1NJI(@S6(E=`#!eB zRld?}o>LRfiYvu}$@IGKZ!g*_(wuv)=$)PIDzHXof_8`i?t;YzSATo>Mha0j4w9(m z(*PfjsQ2KSJf1$7OO16pJI`R9J>ik@`F=~Riwk9pX~1p+m;kqQR*MSGKobUEQ`nHSo_I+G)@Vqg51eRPCWt|No zg^+GYD>YRk7cx$jZ&VyHdz2;e{KSd(!mI4S&Vgh|`jqvccM(wyY^v656|+&1n&{LJ zP7T{FTt3?^9*!*Qgd4hgjwb<+Xt$v;qI=dFO|i_7xGl5e%#eJ?#RxsT+P&#O@+`e) zMYcU)o2=Qsz+L%~oOvvIFW0jBpVQ6|IODyb-%uH&6gqEuw zWQ?9^)=>^wI65JrMvL4%oa_N!N)Aew`XIFDoegD%JKdpO=D7r-iLP{psp0%<q`fN*@MdLr$Ro>q<1+biy+XH78TB#r@uBw$#l#yqrr#$Dd|t3mm;x#Wxjm!3tCjf!~z{dL7$B@ zs;5*&FKs@EKz{=WZ}q30w`zS?5H*cD?s9}SOl9W6pB%(*`bM?iXehK7P`^FtBWkg9 zqV_B{Taom5f5&VsdnQR)NpJpc7qiijvR7FwE)p4T-j1<|;GjZRpCars5cG66nEeG- zVJlA^ZHk^tNaTa(83E}l56>`Xxh*ty)(h$k8BfemLF8rsZU%d zp6HzWS|9@7HY{z*^~t6F=&SWBZded!ejJVvG4y=`w$vbwS&^j0fKoWa6+&UYr6EwP zxr)L0FMXq5y-U-M;VFexfQnKMeX4|>e0!JV8UQ(4J&1}re7ITS1g6&~2TsMT@(540 zz}5uuk-9gf#RG-O5gy+|X`Nw@rSdF(dpP+E`zIv?qajwjB(Q=eqsu!{0(cpGX$YqB zkLy>9dBr>ZV$THY?ty5J!W*LIEMiProw4SR`dM4m!^|;MXB;`&jg-NMp<|Xe%IDJ{ zVD@tn`_5%uoxtguB4N@Z%xKTGzb4_n#5kpXx$E1-Kw)Q7-=rYkDOZyaymQ4!v83w@0S2%imqWx_6L1!p~n|ws%~*czW<&6i{9e zBiutrvQ|V^qt&k>ybdm@*D!DBQ$Bf6J$%0_+Nw!gE;p7Do?N@vIMi57Km`NwhwxqoU+ z`nMPEie*WT5*aj9`me7kYiwQ#87kqR1)=fcO3DdcIXPKP)Ob=<9?~e}zm%~{H&e`Qjkfcw%g>Z52^eWMygU`wQYfG&251Ef;1sqx3z)L18?@6|hj zn5fZj46e!|VA9_QEa)DCUozwr-{jUaqa!tm|)x7;P_c3b)bti4n<*H3}T}6qWdEtRX*%GOPnSsb?R;ebk zRc#h+ZJ^{9DfW3L0j^Hb)(yR`rjG5PVVB4PHr#ib#@fr=mbLj)M;&(ai!M9 zj#|&8Wp|s9Sc7?H^Qj^W4~G_M;mbi~tU?N?4n@uS$>PG%MXR|>Ci2n)4rMHts)P?x z5A!mF0lWZoJl^qgcku^cq9I8A!@Aoa_ptGKcv*jtsD(+@v=*u6SUnTzwsHG(Jv~k$ z4YCLVf1hRzxBydtOAjBwg@BKb$w_9j4=@A?^scQg!(0W?xb~8$efbsTCn14rkcL{~ z7?5v4`AeKT_;pHE?T!fZI-(UVlRceCRgX;L}qb&lJT-u#7-~b|^S~%e}<|!4C z*z3{p*e(}(OyzEe#qF)b+1N$kDl*H{r!oQ+cUA1t)1B1&AN?)=n%&6cWphG5WEj|= z8UD|+oBto8qM$e_2|zjI;BCgU$aG_~)!{P#KQQTkLwiD*AOmwzgRxknI1Zv-LvN+m zzhpC%`JjCM@KSI(B>kmpi*UGdyB>FdJ@)qOiUGjZ<-X-gldKcfAM7dfW8mXO>54T8 zj0%6$a7=AiigvED5fX3$UMeK6GapNZKX!M|=6rwy?w;qNS(wrVB zzz#>y8$JWeW)JtAuMe^iju*I}yJn=wtc3{s9Upl2u@u;m{{b|9dwm!iTjrRti0bDK zT)9qUxXy?CltqcaO7l5zJv@UguS#h7$qJ?C5?wgk#l`b>xc*@vS7{EmE_!3XpYK;3 zG&Q?(mgyAb(AK0HgZ$R^;p~Wm(A=T)O*Zz>Fqz5?*Dob2k(D=YAJubvZ(+6jYj|Qtw~a`uvI+7 z@ZRdo+@yl?gs}8A1cCx5$`EY{6#D%e*dG{(3|}A;!P)ugwd&Yw`?`~Bo1K^Q z{Rrci!%aM#(a8iK`5a~&)5YA6I0+ZVgZbiq&mGUYCyEEwdd{{!yK68+!!84N>x!=# z%;hCEg0r(lqS?iwWjMm)@y*NKwi^x-&b4+s0<6<@Bm(H|HH6z$5XxgAVz|R~BjVcq zHV{q5-AV8?rUy^P z(Xp_ayP0tq*O`d3{*#3N+=#JN9a_|e=y?u1+;A{iqT34Z?$&eav7gXghHr)rl)SvZ$Q3qR5x+X9$YI+ViY7VlTwkjaB&^5zKr|+S97mGL9TzaUMS|O34X5 z7~TvN$1GWpPLnF-aE@*ViJyj=O!Yn3y^v%qk4f}uj7XKzN)=5w3W1pWXhi&R=}(Ff zv%IbfE&-0RjBqr@d911yZeu%#k1mQ#i!n-5t&SVa4jaHay}1HRK3Dviy({<| z@hD{vdmX$r3&EmFP$luQ9h1el?3N-Pu2eeZjFeJXl6tp2d*x6=D$*ebk?>+-X4P_sy)4C7~I7A`L(GcMi34y3iGX>qmh zG?-PQu>7P?BgtN=`S>ZZc3XF`4VIzU3u;=9u#`Jb3%CWm@x*Av1}a~Rg`w168~y2$ zQj9TZ->})3;%bg(H1!)D7%w{}K0YQ|2pxteR<2Gihb?c8<|g%-ri(eAxwoi|LY>q@g^n6PX<_O~gcRbOO%IbBGpJD$!5-eD7=)yIBIgQA5Q9o1cG`Yj_X!VP=1GLU@_ zc=@=JS^AfuJdbSCZEHA~`Ti|d)86nP+&gzr@8Q=FdSy0C7AA*~ew0qq z(-pkgYd^z$u8x*v>(ptc0ryz@#00^9Nv6HuTAPhuLtq;~;0K5GA{a<3(7Jo%lJ3JOw@GgTi`LMhIrK2PbY3!Q=zOo&VvN(ewhJunsJ%dtc+?H9+vHKkh0bWOE0*{>t#zY zN+{S>zNB3C*4GQU9udLUjf--2+dJN{9q%Qa!LzTozK>_E#QG0en>v_|a6S)M1x}pZ7tU<&Q&pWcral&%OgRjCUlVn2SYhYa=m9>u zTs#KuUr6;gv|HNWM^cY?^4G_bNwhNr{w#I|)#x9l7y$+h$<;KFwBx{;iD9k+B)bj3 zx6#PE!wewT65!p5r!S&3ebclWv}74e<&)XgJC_ME(SxQXM3cBM($YNWIy?(FzjhJUJJSr5e7sj(^> zu6DxqxU_(#PY1^(o!eayZPb9{0|3o>m7loKiD~!EBVd2Xh6SJziU*qxLE~F4mhyVn zm>G&I_{r*bU@5<0CO zq??c2`}f@Y+pH*{7Uk#aIM%;0`Gf8$p>RY%-=W@1?U4nP2ij+0FZDqY=SB>eZ(~Wq z3osIhgW=)0B%1OC)nAockthpsnB)IcI_eVI!&)_JSNP6;=$K_KX3u}03KZ%jBCLfR%$#DZo>B`Gn-~|X z0Bce>A}g$JY()6uX8=|KOw_T0gz#u&szIGJT9aCMz0MrQN1n{7)HSMr2(fPVY_uZ0 z+9L%xGNO)Es!})SN?Q-(o(=`rgPosCcTl_RAKSxy{eIqcXjKw?lK6vL2dKp7vb^gC zI%vq2yvwbv$w7+YFFBIQfV+N*-KAgeysvz96uaVL$p3`KGpqt@7c}kq(kH9t8f*3} z&gMCUl2M&t<=G5vf6ycXDiT!Od7e3*=_$ajpyrBxjfD`ficYiFTd6v^OpBA=xT4u& zE8$G6DQG7`XBLQ45oB#}qjAl+G0mR@qFEs568yc9{=EUvZ&cmV)z;T~5gBnUfS}Us zIm?Pp&WV@fK_A7#4jhARjDXW?${rpz7s2la%6O=TIzz8XHEodH;R|Aq9Z`UnbcYCa zYBY51aWJYfLsOk6q22FgkNAG*#6<$cMpSIezt!X0A(rh4#hWz$64Vg|H;V6*=yDVG z@hc}ztt%E!Uv`U7J?4 z`v|IJD#Yyt1rb%m?UiFqZ+MG-h(d29!~E6EB|xFF#KyL?>0Xb(0u_jPh}C_VL?4lMoSvI&^*k$F`7-ik~CTlE7V{SMX<1n=%_4efJ;dP zyO;%GxZGX;j}^&Ld3h+(0_vXN;-AcdCPXO3MAee?P`*V;DtY=nLnFlE5>$OrD^h8t z$^kP9>C7r;cv^YRFh)fzyCQSaDR~I}T5&`PMKJn`D#GM4j+mG@8-b67jSbcx6kw}BvYo?ApEKFtDDc(ihGyD)Xp3dI!olro_K zqq0QTQi^I`KijIEhG$^@DPj=G3GGOuHZ(RCrvS}673r?vn>6jnV=5026cDhBLYXNa zn#l&yThfDAxDgb*vHlK+99<7|JVqHGLeiD4UQXV71Q8kxlTEK!T**3*iJ)^Tx6<$b z^_R1$nBztMgH(q{_`gr#|M3sU!&XrR=WFU}QzRLR%I`2K(3+|tLrTXmnh>3g_ zm<)i34?p98M6E5x5f7XixS^qOX-(gYSP8gbQ&|p60+p(9aY???X-NJ2Br?g4Lwd2h zB3jy{V&FQ{qu#P#e|@#J`~Ho)>)vCtomR*FP3@ON?4vHm6S5w;bpjEAm|}b?ksTT+ zw{049b9>y)@32J8g3T4mVHr2kC$T!6c+P{ZEL`j!K6b45gIKJP!g!hX8w}Q~J6)RS z7Q1G!=N)Y^p7UO>mrSVV_8T|257(Y3+L2p6xT;W_itZxrB=F%km3u18>%%lt1Y76g z_qWJEUn`S*--vR<7uam}zFKXAoIi3tT#f8{4?*l`SqH~$PAFsZEI!xnc*%9h8>OXU`lDWI%H`J2M}7A2f{G&t7~LQNrvdaX?? zjSGL+)X;C6tXQ$Q7m`Nsn?}c9A=Ob+bP7502T6c2dpkndwX38j8fB2o(V9aMJ6&@-l7(SQ8v~5 z!ufBLpsmJOal?!WycYz0SumFFxajAdCT|6q632E6QAK39a3n@08Hiw`0E&f8s_ju~ zgJ0-O1X(8=hUdd@^Vz9(C({q6%+17y-8U-sNh)S)5gR#_#p*DfIF|lK`);wuI93dD z_*oDMI5A-}m(80YNi?R(1dG$6b(QY3NuQLYxC5ye#iXAuQ_oL_KAxGz>hgqXak$qC zh+wgty^!%7^@-u&*xLampSK`10xCqNw&^kxmh5Y+jCQo$0ggd2#W_&avma+Dc~gFW zLi#;SlfP+FYq}~98;qQ|V#~DLSmA866fT*fTJdoh6UC(A(TcmaG4~A@5a4 zCozc>=6?$ZksfZz(U6k`vAAJU!;O8J(8=Dt?LSiD@0Xg7}P1 zX85$5z{W@6o(kC!$2z4>4g@?cQAoilLCfedB9XEcUOo&)f`@T&qL(J7fGW!7#T6H6 zAnswl8mQLgZN=5T1yqLD2U<_j!1JWI7Ep!mpwKMsZhlkAltHV0h9Y7`?Fq>n7r&k% zo#HW4p4Iiq^L4t&Hv)16k3s+^jq}DB|*|1iuaII?CS8Jia`sa z`erh@fpP@{E@TW43asDc+ z&0@Ud-rQ%2wfS?t_DF#(N#owa7Lhgupdu*DC!F7G zQP%`^-!MtDGbMe|@UUfwPa$8+6`&?_r0BM=Xeh)TUtCq<+a`0ME3JIY$t|{NfIRl0 ztv4dc*~2buGfU`{?h0DjJxd!hIjbM=Z0v7d)Z82RSmA@)3+Gf`#|xpF zrbf~rKdc&6JSJs*%hWkL2rY_hT}w7NgQ{UT%?KKdY|&j^CVxC%XCNEbma^G&p1xG+ z^7gqF>%c7c&ea_Up~ZMGy;3^ae4Df=mI1hW{M5utrrcXmlHB^BB=e^m+&I>UwXGhq zHP@hjUb|iN;DbOm*Q6!SwEP=_z#Ub@D?7_syYk0h?Qvt)A{%kqALU67dyhLnFFi<%01WlLuM;4$`BKH%0?%kE}M;ojr)RadzksEhp6W&gkeIp5~PXBlq|8TuGK3??N5VOp~C6w8EBs)a)z_kzmm z2%Y(`i(2y=*A8HY1r-|0i_D+RqP-Yzu41QmId-r)JRQpm%?hV)`2w|&zc_<%z&hq> zC1OZ!=HSn$2mCw{@vDFB))M&KKQis$1<_ZlX7lpzJ8NlA4Jlgf9$AgPN z#6~L|31#r6e~&{sWFtl0bF;(IMp)iwM(DxPC#MNda+RnU;>?pq4r9*auRxLz>THo? z3N`+UIwOsW&=8cjgco@k<|0&A;;_-#JLyBJSpwqAXGJbr@tg0;-|5U^mjgiY?Xpy*QeIQy;xZ~#WcL@|Gb z{kgn3QT)3l1x7Rz9#xp~+9Go$_=qIDW>=@SL%rE`Uu5$o|6mVQ#~WgXIv|0cL|%pPx#Py+=A!>mq1n z*ste^mjd$Q!-d8#sk<0qg(kOR46W1o$ln8VZl^T+C@YK|gIylTk0b#{?Rz+Tdmi}^ zV>$Ssz4Vdkma!koa$?rx*&i15MGPGHgNNw51GovZLWt^>r`GnFa^QH*D`?vPhx;D; zXe<5)iIU9vi(*ZwMqkZf74epJgtlZ5PtM2g8mT{?GO$GDbghD~D=6{0hH5*XoHwzQ z6MM_h4W`^kr?M>6ur>JHDEo1)tCz=Ct~UyKd(P&+=fF~<<}mrs9BBNx|Jf<_|Hj)t zuFU^sWQdaGmiV##dLboXXNUbqtIfjXu5N zH8sn4hU8OO`UyivlM@D89{rx4oj3%7$e)mvK{|0(r60g=3Vg9^ zHVkDgS}1jFPT}!$zSWzHBrzsYxk<#->b-eS+ckHF(Zjun1a!2T8uNTz+b2(aMDK*A>A#MVe4iuW)KwegVVPrnm}B0oC{^gRwRvc zACZy!2vT#r-Glb;v1f7L$C7j%0)n|Q>(SmPBxdHLB`9b;->wk|ob^3h<`VUs( z^8Zd3|5HLu;onpVlQ%=(IeFxWF$q(VTyun^CNgm;a|)qS{XYYG=z5Zz0%M9|iW@bz zr0A??!BJSfB71#}^j{?BttsK6Of*NC9#?K&$DA%*pD!OEJz|ChCXMRUzM$C8guY0c z(6;pGzvIAVN67J{-(w&2M>&EsK*|mCsh}unL;71a z$(cr&Oe`K|B+`Bp1vY-Y%pPHDDbNw3lIMjSF!GUTFp{f_;nte;uWY-#d~o(9kp67t ze<7kE(_3X**h22LBgsVZC*db2bWW~LxtEslB685N#R^4er?%4CJ+5yPiq989I-s+@ z4MKCzY4H=@_4~S4#!;NrUY>t*_+`0n z)&fNr(4RPL+c5Rh!ZD!ck`+~re2Jfx!Z6#(B`Ml(FR0wz!6zRzA{Onvf@9~upK-h zW(cVG2R^ZYM8FXVO?NTpzt`3*1;<^@&$@~O|G)M#IoW?$Tv3V^4hl-hTCA;ZTGk_e z$tnuSRHm&;kXGc?sNw<jauPHpA&2~6lELaUyKW+rF3zQRt6O6v`x)J)2hq~GzZYW>ZA>% zV@9>~A#iSXDQwmq-jocw9nK^=_={1r`zszGsmjr1MjPX``@$zerDxAY?%iaEwA)Sr z4QWg|q{8}xP>6F|Cr>67Z*oTUov|@P@(l9KiNt$`XH=Fl7ZZhwMpxx{I2%1}!v(}@AT){hH2Xp7GLLYlTv1%#`SR-BbTLr~ zP_20Y7PlCLYVf&+e;Yt}J z#{z_VC=;jVWR5aTI}m1BWJ=?!8@k#Brke6M%H%`BNa~gqf&o!Vs#XS3fAS?W;G1h9 zG(cF^Px@f7vR14O2(uya&FX{LPT^G}oIqK=iU+t>-2l?!Gbm}3XaDqn9L>amgrz>^ zGnv4KZ$bg@BpjH%5?OH9!E#s48YCDF&-&vNBV@2s_D$o>Tr^Df+J3m~BHcixO4qwR z(Ev6xkGoGI5IxbWle#(o4z?DSh`s!sy)zYziI!aeVkJi?j3`lece+BYeZk}Y&K8`- zl9VY>gF8HTE*5C3dS6xbN_`W|tt}88=+Valkl;oy;?vwZrs)F(Bdu{yaW8pH#jhe| z@Yb8-^p*4gA`IV4Zp_TDr9ErcHnL;VP~2*M=6+1bvWwAhCZ1j+q(YD^FdoCQM)I7! z_{w#B=-_Lyhhn$(VfAc+psl{8mK>*{sBMU^Iso-Vce{jHi|}aJj;rGbmg13#@j_b5 zUiD+VL{wQq&Wm2tog!d>iAc^#=8<=o#6L>4uW_g;qOa)064R9?V2j#pJdAExqYEcj7d($nmv?+8e_rIFs}j@+f-uChA>>WG26MeuQotD+5c%(8@$5Zse`Sh!x= z#P4JdFsiRf3IB%hI9HgoSpMMJ8nncTF^HfsdXKcs`KHW}$%c-xQr!m9t*j|Mp-Q&s zM--shnd8aQ7uYE9ieP;m7m{20A`B<5PFN<1${e-onB@wamv#s3+DSub-C_jy3_``J zbK!aF7X!Aie*DB+{go@B6=6)_tX4FCvbb>cUC6?H#zy+c@%|bPuuZpb<2JQjTXpRQ z;Qd#>x%3m~Vi;P-zugtZ;i4$V7J|24;i{I=yLb2nCgH_v_T9lCSH#+tn?Skw>TjE} z-UX4*C2?DU6R2>~8*}Vaxm9UHX;bkWz+#*@n34$vj=WZ>qw1w-p^~fCZD7^i{m6;*7EXzmU_sh&wqg8{#6)~QhRAt z0QT$GE6lH7ME_WS#-|`4CiX8CrJ1AK|7WB8sR%K_@SeI_$;B=$Q2CM z)U_w1>z5|m36D^xYT_^0T|O8%kez-2JRY(xIItQ3k-)^eTpccG*T8_635al6ICDB6 zIy&SY4(JFYK`wPnVIWskQ{17@V8>s2*zgjVgS*?5u zy=MhU%?d)CtsG2vayYU_qERe4GD;_&jP_dv3D2!BN7mj;i*s#O8aTuCr3}ql zKxdhaG$pOl6tn#ZR@U4yp>`llT-FYNH$H>#YmZDjy~&%d{Iy`GR9P~E;p}j^Tlu|v zHVhz^6q6nD#a>QYa9rpHxmF0CpAPQQ61CEaIkTw?=KDCR^)yA0*uK&SiJg&ygC{!t zAt|#+80Q|qheKw|_6c7~ z{AVgbi*^hAZ>ZOc_6^ty_xE3bi+#=&8h$Cbe3!8I?AcH@cC=SW4cTX&WYs6D^Q|oS zQokL9IgZWmN<~GDA}zIKz9TBtPG)*I8LA0;5QK0D$@4Z>INtW{5cdmGP@1HpI7|PH z<<4c8`wcs7HEtVPi#w{(q{dO$IoG-{lji_N+Yi%WFgQF7m7M7B&-uDAVI#DWF^fUA zoww5`T%}!V*~i&>CIlon^LtYaSbcW2G0qv_%vxBdZ;aCRQK{ABulF>L%BGzR-4zT7 z?OssH?xrS!2Plg9hQeW(1(LQAqIwc*6^1$+&NIf=42Weh3M)$vj7~7>GuGFAixzMP zpw2e*hmxOJMJT91O0Gicka*nC&AOeEJD&ELR$#sOT50fgAHR@mV0Kw9#us6PGxWEv zW>)5>aN3jutObmXEVn&^wm=p|b5pT6jz=O4XcM0hrE)GzlBo!@vn~t=LLbeO1uJI@ zSX2v|3!xv|PxU4dhb`Wf2-;JAhDZ$<=}d%z>o1d;kYPJ`ouPo1(2yG`>>(Fh;N{r9 z7&_k^J@9IM;O?NHbM!%&f?Jn^gMle{K0=fEP;@5Y82fm7X`1N!=qeZ)`v>|i{%Lp( zyA+3m_7@tl_>2Mc1c&ah$wW>0HU81bu_^;f;-tsUsZ{ENA!K+MNhfJod=v-E z{N^!Jn$hef0*@82q}dTN*d_v7jPhdC+3P^6qApWK|07zyd(N4${(FD>k<|$@%2$D& z&e+T^)cAR zZ#3_-r&Cm=HaHq5S_7>pbtG5)I1y@?`)7%RBZY%Q zH|%~!wlEWVzv{U+c29tW7s#%m?&|&Z)V_$8K)m>B-rjrHp!DS%tqwo4jv$N=7+PIa z6$z>+vE-%%LhcSFU^u5E&!ySTe#^;3z=;G~XZ9I(3R(M-Kcjl&h3v^)Ldi`M$zMWs z3*!_vqRHk1ipK}8Qgw<4s-pQg-$8|o|9axW?|?$jNsi}hWGBq-{oI|M=o=-Oat9~O z>>H4ZawVAQKoPsDX7Pz^u67^$r$;QePOOP>f4SIqkd~Y*5-eSWj#MEROi1SN2L7$nMbwyFJDpzzS<&X+2f< z2=yhF#hs*JYQC^!{=7;lA^bqO_4fR;ly(5@{?eHQR&H86SFLJ7_^SMny8p@Z zb)Ayx{{0`j{J%K!pk*&}fImkxx<5DW|K8(ogDsWe|L(Jk?8*eKe){# zHeWQm@P;OX%Z#dWQvyx^&Oaki_$&q8Mm(BqxZi7Y$OjDP{fjq|EyLYOZ|maAc`M-c z>-7WBHhl9JE{QXMq+r9C>Q%_ z>~cr{^^5QyM^9Epmhs;T(tn3Yst~SQ@PPO z-LTo@t!p92i;0Mq0pCaf+@LjKmAFrY)3~cu>d$pmAL@=X&R|+EbQ4ZDDO|`U@Mz zAwZ3}WcQbF)^M2E(uQe?Q#E_aXFPbIKl-}xOUt7GS31vZG6z|yKYw%rMGCnUFjARUe z?fFFx1Z%!)8iN245*cW#WuVh(_`HEV|2w8bO}g()9$jIcE)g{R4yvTifnV=Cz1N!6 zO^ik5K{!epl9y}Tj|zSj9+KafXahq!bi@B8+i3)Fl=AqAZTfEyc#w0}l7VsYqQMnr zvPnC94C?LNurL965rs2GDJP#x8p`DrlR>b>y!@3F17a^Mt~3t#f?=+aX{2Uu#IUe2 zaeXFYdgaM)e4)~a(R#vDgmy|X6>a4MF=+WYt&)-8%PN$iDBSw4fVPp)Eg54-!ej(- zvmJ=KTL#yfc~tey+Q00aRJ0vzmpC$Ul&7Zr_0gaci}W(-!V~i7&BZMR`@j>kw}`0c zt{BzuO!uQ_roC`Z9pTH4AF$Quu0S?tFZ4@iu2AD%t{TIfY)ZrJt}?^#sptJ*9NsZ- zC}VY<+BkUld9vJOM++hqEg0hK&tRTZXm5nn$%na=kum|cs=u4f&I^sHj0ej`z5JmT z?Sj#I-xH#F_h~u4!V4kaq5J4$8nRfm;z(N0NnbMfjhrdaNGi~4ziC3>i|~Un1oY(? zd(o~vTUby=e8?mxnw<1P5Euo_@cD;%Z)tIIAcu2y9dL9)Idt|Y%l0>FCLKS@)E!Hm z2~(osOXinfghdpuJV5p5h(PVP;loldg$sy>e#Z`J6~Zi@(jitCi7ze?JNlwTRUlug zoU=ihoU?&hIA+pL3Ndu3cG1KgZL3lwYnRh4$;oSS6dDZEY~ywh9}HY1MdcOQ7N@Pn ztsfc2Zs-ogho~th;GQ1QYFL9Jd2YEKVQdN$Wg|pw9F9r~@8>HMY&6XcI^i0Y&|{OP zG!;eeLlH0smOO+javG|)^1wB$h(Ywz+PoE^MR8=jZQg9Ua;L~G@sQtc4YiD3W zP!f@^KX#R2TXS}ix@fm}0s8LF`TW$}a9W&r#(1#hl&G8%MRIt-5Tq%;O4f3)e+o*M zDBHrqP&To7J~ZnJo|v81c+H6)fHF&~sjMW8(}gU{t5mh1b?n)nx;KudULzk~Ku!UxxfV$-N`#dbi5^5UVs5N@W?yS2fchtRF^77o`_!n{ z)O|CHb?!rE#$Pd&_DAni>!G*{Vat%|?U2qFwGJ3!@6!Y_{IE`RN_{7jIVi+Gkyk;{xQ7yRIWPH;E&uobgYxB-{4Qz96k@+Ccf|jMi zCRa|LA2J$v+v1U8%U$LY6fJp~Ii#gXZB7-t9#WacdyPvsx-OP5yU6ouq}S`U-h~Tc z*=Gh_E%Qu>7#4jW-b&;ro={X@;lqu#2~AhWxi z(p}2FSOvnM+R@BGx!D(sFd*&yH};P~ylV%=F%8!HP= z(7u7ju^g;EMYv*aLX2OxPc2v7#>r#46^3q6ccYm^I+_ZJovrTq+fmOF&|3z>B)0A3 z#sSy8@8`sQsDgAFL37_#uU#RxirF(mwz$N&tNGa8`|q5~Vy;gN6`tcYw8$WxIk>L9 zs%ks3P9qgb>|=)W-HQH-4O`>0V8&S?jS>L*IHkWH%OEtvMnD4F;O>|B=L!?iu`Q*Z zH>IA)W&TVgtvRCWpqZbjI4EX)css*rgZC3a?FC0xWM8z@9Nn;)2| zf$p|Nl=jh-^6P;D8WP2iyER{lnOy6iMFqPS;3F;F`F_aYL}82lfcbmNL(4B zu&m%&abIO1n}2Ceu#?l#x|Aa)V@|ANDJlqe(i`9i7Y}*`zXoVzG~IiPVb4#TIT_O~ z134?5h*e8u%J+=R_i#qa@2IvTuk;ig3LzxK)-0MoOJK~TtZg9Ewjo zGnf8^>WjDw&aR7$2QO-ovYe_ZTp|4n5~)?%Lz0h>XN%>j#o7W9?l^ppG;0f3rYbgl z@-})=?pViVF1Bgz)5i7`?CH{3GzNfEdRlIPP!M8+m!)kt8x3tYcT`)hK~}jrA8`Mj zqFIn$2S-Qz^=ta4nULWBjKu%PHvd0?c$B(_uHs?Rcb40MG>HJ9A27cwQ7C-y064h0 zpe(+hDdNm8`W*{V>%Kn>NG2p)p1J-@&*kc98s&@Ty33V+V_Sp=ttpqF(_R;(;Itl!5us`SlDM7a{}H!aJho!*ufv z@If9b_Cgi<`>R6Y8*$*+-xA^WCIPOgW33LCN8p14KjMB6T-0|a-b$1%bTvMok$(+S z#C7y1_r*dQy-bYc_*(}L2_`L(8bls_K#W0cxCpp_j7*_WHARxq&>cR)DA;>qK_sk- zLa9J4I8%4z;&-yRw5)U@d+>x(Vx8{f5O?Lzjv=M#RgKi*Og}GR=(8;yvnH*r~hXy;9MTKjy6j z7@hv6YDZJ13`vID!rlYz6RE2d=*wm#331N%bIGFP$$V*>chZjV38!}2k$LkWtj#^h)LXgbcI5{rF zRzK9S#8Tcoite97?FB++DKbR;>^hFjd@EA2FKO{1zwiCx&(zz_Ge@U*rtpkEM zY$`e|Qk5@W&{;i1gAt$$1EUwOxq@IMI&xh7K8UF@<%sAVt z8d4-$%u>t|-ancOV(O@LVbWG>C$SKq zp`Xao+N^cvCY(LMl$cCQ^QcLgF-(J$oH?OG%8Ek=DkvhH%f^N-_v^HvEk3dxQ-H4^MgpVV+0nikDiJ;T;N zLZzfJF@`3cXo96F)9j5?%v;Y5-4I2CS>{0ZQW#@twHnh0u#RAa)Fdt8K8j-4vtd-A z)&Pdus0~3EGKq&@&%+gPXPCt7zdR@!CUU7K<6~0C#unwq< zuwgXFG|h62sD}5R#kBc*EKFspkBz$CxwpsBatot zIn&BMmQ1;q#l=yX+$_=y-5_KT2Wz22)CCi)`A6Cy-h9lV7HbgyH-gka5nO z6z;v996@Szd+ZVjBA&f);;{jmWahNim13>QVrRuV7jI&TgJP;7t){Qtx#0$uWvCsA z+E_|)@Ce4ZEWyzmoU?#Fa6Aqizcl9ZZ6(~Q!P%;x-GT;sltXPAiB-Nm$ylSHy+8K> zz6xQC0+(>Vw1ni!P?97+c*9Jrpny5OX6}Lb0e*OWXpsmS`23AB+R0m5c~&oH$q$lOOsPzU|=_ zJh4@iflrjC9|k{c#MoW6Kd&H)BDE57odBJ;BUO*L3xOYU6W`28Lh{oSZ&+YXSVWX_ zmhA2+)^hfN&P!65+0Y`iG+2C~kdi26Upo7!!JJr`ps-RR6OmGFZmrU!dO%z`SXNVh zkUQLlGu`+IX7RxlynAj~!IguEJa=mScA}?>0X%cnw5FUTnPGi05TBF3PQe*w#fq%4 z+^gz$Zl+aj(_!?1uZ`PS`DGwm!**;rzkSpN%fVW#CaFg-nf|=E=+hWXpcA1Q-)QLX zS~#0vR+|AQD$q$mf^4!~zs3oXLxR~!p!Otp3@HbcO^VS`|&Yfq((I?$*8QV_@% zNrgnd+J>zLAgw4UX03{Pox~~73V;1uq9?XxQO_m#fM%)2REY(8Y_pYy=xJ1SscHne zjWy8a(Q|V0MpiUr73XwTVq|iby2p5Qx0am7rFP+p42#1e_9_?}>PF}HSM=A86`&Gg zEi4;98XbZ8yqRc^(~=%UFg{6Jjq*5(mG(N?k;p>5nrIR^?qFvzkN9}l4|5m}IC%I^ z3pny_cniSw(=i?37F%55z1OmvyQ(JtRhqH8X18M)j_EoND#Z_mV8bbD3*i z*etW$WumECu;x}xd}u3@Q6($zr$&zYFzyGzSYa~S@c3E#nbT5b2uTcCURBU?7T#*< zd`WT?!%#ak`6bNZg2TgikrX-TvOjH1%y zhWx~}Smn^-M2h6prFp~HRS>sD&KoePF;pDl<=kqTk&)H7Av&wKVaiH5)A6zwIq;G} zVtbM>IxTRSm=Y}r4`xTBWg~@4bC5B6(faFcZtY}I=khHdg|&~ovdrNwI%NEP1tyEq zN|m~1RS!%g`DQn04T)^DJG3+P%Lwe&>;*1`2(Y{RSt7&AFO`50)gv*BEM<9IPkZrb$YHO z-Uj%H(}tnSVpX3 zb|4a3Xa{y{+qu5`!P+6}YL1Q28rx(V9T99UPp~(5^J(0bkd1-?I1cJS0S}e6V*U*w zNeFA>^^q2dR*)7BiX=`7x6Xmb)$F_&(yAG&R{Wm%Q+u5hQ zT+i?V8`7iqdlaR4H{_XOLGIN6kf5C8t&`_~h|%WRzWL6nEiH+;E06Dnx0E@|G3rC! zjp-k7_h=a&fAaVA`H1{i+ZTWox4*uZET{kJ?jB)kp4QG~U%7jh6BPU3gZSbPvXj2d z;E&PEzDbI0Z$splXqA@%eISmTBCz*lMB}8V-N1rf@5yB#)r}Cu7(E!x*P2#T2P=fT z$NUSp7)gBaBdtlIAEjORpKZCkYrRaD2^SK>k5K->^Zs zR4>$+Y|bTmvWkyM)^j}51W#XY1GAc`k*o^HhuX$Q2=PFClGKFWh;KtT(l&n2I#Gq?U_7(>ZcYWAM4C#2t-_IjR;{;MEigF~94CdjsYq9{W`sf8ADQ7Ou z(imAE4Z8=o2xoavEo$gf17+ny$~$bqv@f^JxeI7kmU}}gtL)_vM$C$L;cfyS+GP3N zV4UZQcVt=vE#4+rhxf|h@NcM@@TBtucGbXv-YELbsKUW`+QWstxfcq`7z9;ZgI&7P zxh|wUVMS$Dr7=-rTrNyN5sFO1`(_2_@o4Xr3TI_C^=65Pc2turt-X8@57&UU@$d=n zj{1s}{ohe3g5#KY@z>jp+EHHLW9&u6OpP%S?h%m3_((MH_Q6reLZgfe9lLsk9z^$U zu-c5urhHV&r@VDQ^RavtPKLrQ060F8pK#*$uTPx;i%*1nP4l@VSnXIpy7XFZ|;5W84p)YV|46Rp{gkdxUdOnrv>L zSAnc4x-s2_h$d-@%Xg9}upHscYpne5v^b}+e%!r===O4--ZdP3h(!WwXu}*Vpe+H+ zi&Mgo^k{-6al%?@99a!dm4U0eU!pBN&kmX<*1|4MD+Hwt$EqGod^Mutk4smrT4IK6 z%SNP^J!Lm>%l?o1p#*!_?D!7b)Q_2QujJbfG!;m+Y}&mJCOhbX1$dpuaFRHvF4kmHdBEiCKaCob(NvSjRy&csXK~gmx zBi!#`C-LjvY|LE+;qfIZhOkG=OJgg-sY^|k*7U0{n7>7ZO~OCyhm*p$U#5G!3{ zyJTT8jr1Bn{TXn#NI04zDo?RJjO7O9aA=p_4puVZtu*H6*q<%=sc`S=7H&^mdnO5;MiX42WoCzP2iEW6caUt@sPn(>R~s>*y<{-(r$!g z_&rb#QIB*ke=tcqde?-4X5r5BWu250=EDo&QOh^MZ2az~{B!EruRwVfc|=cckQn2v z&)$1hIYk2(to|66$RWs%+~L_?CveS;F>UNg<-B@ztg`IdI-PI3!)>4~XuqY-zMJ>5 z4!GE^VY|SrI|j=wEbS2O?f@)RN`@>>6+U^TR?R!`@Q%a$aKtqJdmi_NoMi&j^sqzQ zJGknOhvOxBB15cple(wZ8pAfGb12}B#X7;oE(L3(>kg-z23L)y)&EFGYE#!l{kY^W zG1cYXL9Rx9?p7W&ikIH%iiPl~I@N3QoVZOWM-rRGovK4IO!(Y7)rVy9toZVx` zGqx?WEyJ>tJnbvJwr7;h^wXObb&j=L#lu0_QM*?6mz`R;*I4IQE0Nzp3<6{ki~=f6 zk2KMosghZ1cxYDxM$2g!ogK&KnG+*I?|~+V3-qnu%GGUIgK9_D?K0F<2*RnCjJH{P zN4CE?YP6og;CILL<{;GZytX4-$9gU8dLF>RBLUzz*Qk156j5E%ec46^=Z7Z2*Z3%S z8UnlrqYRsd=&2Xrxr+%|_!h|#l|JGV5X`t=Liu0_5ku9)G5-CAP@#b2xC{^HbcHUHXI+QH{o3IFaZg?&Ho;{E%+ zlD&nSRk426zTVyh zySm->-CCj}dmGvC=<%58I)3Cl&gQJ~ysJ_aYK*;hQHbA~C;n7=-Sl}VS% zVJl1JiA!pLApvU$pjI=im`+b9ZkQ5Z_;W=>yUsrdLyEE4=O(Z}S)en$vT8ezMH3q% zE~7CNG~ZfCKEDe6(x&DtojhD{kCa6_=T<(glT~ucrG1P*nsNuVN~b8o12$uSV~L+b z$aT!|te~D*a_^Znfu(g-$l*TI=S|9N>&JvFQ+-g*RlW~0uf*w^Hyz>FE%gtT8}*!9 z*)%f;?SfldX0yPtZvd@}Ldc)o01G4gqUj1povM3ej+(hPnTP6>;-AKQ8eptvoy&da zjyl(caHuC62RoEmi;~x;o%}MU%WehJ)G*dg3cNP)R;qnKo$5Tw3r9Q7U@mm4h;_3x zjAKfRos6!6wH)Ro73+(Ohf>Y9j(9=LHty6|71qB=0;CyMlOr?wyl4Lylob-(8HTY8 z4a^-ZyRw-L^T)Q7IW6pAXK1jtFxnw1@_SLGi-8u5QTK_BH~!L-C#mvwxYrZo3nB*a<6m4Okhq9P*zFy-{G^&?WRQwOP$j}tal3W7#IUiF!JE;HXoR? zwMSMIU45}kZ$9K=q{c^Fghh>rm)LIBxBY|&H_bbnS=6*p?m`bO+;P@bH) z1KF}}mY8tGTR<$dIW5Z1ggQAY|4j{DDnG9^KB?w)P&^J6>9HzkDn>X~kABwh0YodS zDLN-E)`%%%{U2M5!hDPr?v;Knw{ghWmJivsOlaWOh}aY3Eo^ATb;9~xfM+*1n1vi( z#cW7W4GMq&l^0&B*hSb70Mg>A7iXtpMT#ZRLt5TdCwkuj$M|&QbN)f1AFAlba@P{( zcRBsXq8Ju2dQJ#VA~8#F8Nos_=yh4h-QfA+9QT^pVtg>zO+Ck12(j#^o&V}48uULp8o!%-0oKiJWUJ6OeBG8EXB1~SI@sf;n};08l2 z@LYOQBdD}eSx|_Q)cz%P7iIuSk{ll&5da7T&+Qo_L_Z}nGi!$qrbqS>QH2$*C+;A? ztOGOdUj06xwiq{)_`?RK!|;|XX%}ro7a;~bomN^>pA7uTUN1^VEEXQ-tHFg|){F;# zg&UgsSoisvJNVF_S^BYf(hw|#Ts?|^;w)W+e) zIiP0O0#Ft_+#c=3(-!}$TaF5YSy24qiP(TgJgs{b&eFL^R$!TQL_6eKB4<4O%p)YA zhLvqp8wLh_W2%^+y#41%S1(AIDTcQA#P6QSDvs8X9UTrJ>b99&ZeU%M<5po&W*nFC zlm5oJpxa^@S`0C0PQa!ufl8C-%RI7jMBogK83E{Jkr(I((1$J7h+QastM0|;u_Na~ z;cH`-C%0|GD)n0jRCGTkR>5`t?#}?13QB`cr$ghLAQ9Sq-i& zQVz5vvY3vE(ddq-n^Di~sIK}@g|?UF(Ikni8k%nyj*7I2KjQ{zQZ=BrNcpnHOOt)@@Q`HF# zM)b3UK{6wNCMaW7+f{4C3jN4L=}jiNq4-Tk>~l%-A(e{ItH5kDSTbBp63ekxIPbp<8f8c z*c9#2No|!xACayqNVJ8di$tyUBCcjh^~$q@`pd_B4IBc_vU;+uS_2QF%?INx$kO&3 zx1V&(W$~Qoky5S^(qwy%51Uivu?-VO$rp-6P6_VnL5GH#vP4O}m5lhX&4bIeu?L1N z)Y=)=@$W+BPF@W_`;B>Wb|_*l4D31h}GYc)q1Zg{;QFxZIz1fx7^A0xvFA0n<=1Svx+& zm~8x#9{a#@(Vp_oHFS#m7!L1UvRBl+TXVB7pd#vkMtn&}^^pqnic_58Q=h>G7pB3qfrI;XPfoZ{c zVK;31`p*C^LFBv=P;wo0#YXf(kIlfbRmZEqswDua{2agVQFj7OY88Q^f<7a&mGRW$ zlwE@q5TD=^UxF1{v3lKLOf&%sTu=<-*g@v`8ya8an%fc0#^(~}OPqe3Orn`L&HkpOWS;&Nq?vp``c+~je*rA;SuQd7B zHKkD3{zlI_;&yfo+pl2gm$|c82*!i4l0DK8_;E5cL)2EO?3z-<=g_pc+^(x0?p+M5T~n??1Xv=lV+UQwG-OA$;RbO4cT?P z1=6UL8hF=+ne1uB<=wzK6r#E?87U@@75p;z?$Ek5f_JR3+upujU!mAOA=ozkXFf4r zy#3KFrtH}jl&2xHha(a;xJ(lPSHE~8?kv|VP~>)YA+Se$PYP`2#v+?b>SQ}NPF!mI zs;5ZHU|cJ=)qZBd&hjf*25&xDuuwR1Pl%dOF%dVIpUTja$o%s0=gD%IkLZ z>nZ7r3&$9#ve0{_fJY`CR?_xejC<(`EryoqDU*FOh_>!q+eRmZ!!vlhI4gD{BmEiN z#e<4i3HyXVdQy_`;^OqYRs5yV!55tr;!%d&@#Tx>E&oO%8hNWEzNpI|El^4%uCfci zU@AeF$vc-e$|$8HAC`y3BJgp0ME~GPA8)At89t@{Q=(xS2YT(+Qdik*hsO!5gjz1c zsN==688#s*2EtRDC(*1z0=Gc?PMM-eZT(qOu1Jn1K ziQfmYRark1p;XLE4-nvIf;9VaP4RN{NnpGRzElVlkW0;*|KIre5^hR*iV!E_M#!Jg=fj^3FW?grw<_9Kb5r0sjtg)1@jzH(!<>Ozty zh}9u8l0X*fvHPMA^y-@(`ks1XQ(k>$b*(9i+PPuNlp254(Tb`LpB{+#(;h~i(M6FnqcO|l z=OEFeE$-u;b+~;dLgo)Ln_fXfSnM6lQ=KhI*gNS5x^1bE7Zmrnrv*t*%C2#+6YB1$ ztzFI&gYIu)A1dnYraB$z?APz58qigeyvFA`cV8qET#6xAIPoym$jRqoid!Av3uO&M zNs=?td=Hf@LF|^i zoHLDX7aqs9>FyXXP14Sa^T5Eu&RK9(cDg}Zeh3U_a2!x5Z6#K|xw9~LW#1M@&OcRQ zgHw5pxyj#sGJZh5v*8MV^^^mvh9iDS6SvaCo|kUnOd$PzTb$xsJhbmJJ#TDBadtGX z7RJtESuw)Sa$ebsmI<*C7cIHcSerYo?=YX`(3+S%t3EWn*P@)|(B^M^sitWRrVMQE zU5av!pAgbKIXiXrQyG^`N{YIa_=In~EhtI?_n<-j2}Pn|9v0p zTdAPvXk%~muQ~-A21XkOw^b4%IB+-={Io!!p&-!6afHy|AHhSu${}NfP%=kD>cCB{ z&MO+!OAuF07?x!qalpdTAkgNhO;uGID{Dh4Rck6$Rr+7|TN8$2!6z#*w8vX*Pa6%J zUzrZLvN)bQfFGh|61cyfyMsBI-g`qVH^>iYXxqKmu-83Jhc?+WKjcPZ0xePR4Dg?R zzl6nN3phixsgBa%KY_1O7mkGJT+Dq_y$kG0P+><~S0ccSJV!%9=8lJCyIcn&ymT&} zcICN93Ux<+(&9Yrf^(e?%-lH8ehpSw<6<^Sl+-_!@Idb0hjd8I?FF?$XOUg>%+fNi)XwR@W3ne5Xw{2LtH6Atc^ zki;6)Nqk_%{*f;B))DODY1-d`v30-8^e%|?QG)f6V%f7h^hS^@!83c4=TC`o!G!ca zbslgGc23Y|DynVqEoyL{R2a2lT?S2v8$@7CmXni)xU%hRYD&9T=2|tPPGJq1oNjkz z&{#o-5JhY|~dC+v|FvSTtXi$u!h(?t0NN5Tk~+J)CzWfBa_)LAK!NpNzirVfrQ zL*|D$i4b7rmoUPfoNNvR33~v7#{^ zJfq+>Nvy|)QuFi-h3Qb5HyTm3!<+ydtXFimmPLr5m-BngDnasTmhz)A!vumXV37)@ z&l@mMBhwVOLmUI8wQzV4JCK9CR7K1wX~tH3ad0T>voy?#e1?pcIvIjB9kQFylZs#pqoWVc;G1Ca; z691y4Y&0A=K63>xjC3;JsIb}#? z$P`S>dasDCMrq>c8JK}g!9xkIh7G2!Ar%pp*N~=9&sc20&2s(>`)QH1RgGRBi)3FN zZKPIh0HSin24uyd0%(e6ycNba85>@$w~L91a~ub2E*!C2NW*bPYj1fXV^9rOE@ZrJGEm}4U-|6 zloCVy97%8DvTRf(nhgIfsVuTjq;JGA9WYp{f{{@jEob1Q6;`3<&ZrC%G|u#%9dBq6 zR^I|_R5Lf9s=uVn4ub$hVbi-PSOMiR7&;VC=c3SiD{L6GaLWw-?uy_EmVq42=D9Sx z1gwl-LULvvpI%u(Zht1i&J1`i6Absub?~?I zw8VhxT74(#;de7-lQ`fC83yKJX~<$(GfKCS^EzyAT6POe69A7LmW+ z7KU#vBHceKZ>d7AEkmxcj$LLk@L_*hK(QNiFy0yQoh5mX72`H9-?8l{1TgjMPTAfs zVBLU}TfApq=$LX+k7d;l%d^?n{+6Gnu#`zXsV90VBv*)@+!Kq{BqFsMQ^!@bhskv( zYeU`g-EP2S_v=5gJ{#YP+VEY2))vc-edk-YMjEY@6N{^T4nl%NC$=DF{X_V&$td@rbAudqNM-2tBytX6%nhE>&A*LuotZrZ{m zTNv-;E;WGbps(6W;J=*9UtS0>ux#RtEBZzPn}dviIyJSUh_aWqf-MzF?)F)pu)%vX z=`qQOu*={#AL~&Z#T($f9PhI+<>NrwBB0n8g?-78CTobNz>iIYT_d-Kmi8pNevr`n zK&w`hu6cxA9Ea}3%;t&QSE)wVrcZ_l0dXw-ax7DfQ>tnWUCyeR?hQJ#v{X>t(RX65 z&MitQooZFG8ZrW&Ql&a@#!vCYm>vOjB?tUu+56>6W`aXwXl=@sB`MiGxH**?DBYU+ z3Eu&vc0Ad~VZyR(MDmhhHAUp0-T0`Y)z8dYJXxQk%c+S!ilo>qao_5fsS zVnaLA4!!W}2_V8D^UNulYhM}VTglCKB?Ne|A8^sn)lewXQZQ;Slt`1bC*}i+Heqop z$lvNhWn2d)(LE9N%6ZS+JmRgJ+>8k3S#j2Aj9(%QM69F_R-dk7$q#*yPzkA!>-9hj z`i@ZBhaWm4oF6!Z&OnrXC|hqT*_~k~HYJG}>HzD!L{0R#^^;p{l}1#Cc~D~QXiBs& zVEH?vtpH7e*mCR?O=f9z#xisD`ZYZhuwRsfn1{W> zDugjU&3m342YZvJJz=jcWS}?X^tG zQkOrzTj$(~n-hvpjH9r%r97b$T!Ra+iUWj;O)>7TWk$HPJMw2|CCMXXk9PhSVM_Sf zW4D-6>6Y2)dLU{7t!v=6L7rJ`H|K>s;43uXqP!NTUZb47^=i~bDUzIj^<^+gU*0L< z*y`E-3?O-=X8gsSH(*LoJOn-cb`e?_BOQ2D9yh$az}Zr>~EodfYEbCq20wWj@vZR z93wM1(%V!40~je&i6BbKY~4K)c0zeruRW1B-L_bve?yO zYO>>U8p)G|PZ~ftZwNeRaR7~5X;51wz-%M2n`jZvUPdXs626y96HYvpjj;Eo7=KZ5 zQ~jPF*%7m`FGPiwxbDk`f{>`{rYp@QE6N=`BRYl9x(mx>&{dZ~QQ(j8D#O49iU{cy zjw*V$yRg}p&m}1YDX|r^ZN6KKB0s&)bbY(TNH=K;BkodLVrgH2i>CM}+D ziHqV|^S%;6*fG@C&f5z%9(~j`YW!$R&vnIf-WcJQba-;3Mwy{-dn-3UJ;|XtQMLTG z?A=0fRI29u_Ro7c|4|UNWklSte~aE5zQ2F}u&-d`U~6OT@SpXNQ|ydPKOfw1j}_!^ z6n@|@Jg}JlzH-+ES9o&GctAoX{{Z+};p|X%s|RYliKM>6DkL zD{n6!&tGgpHNgxl6=Z;k(dM`;h<4-)2DP&^KW+v##cFmz3#Mb*4M;bx`(cix+l$1! zgC6rZddK8qr7h*mgLKb6!n+9*hUC}GzO8H7-%feO{(1EAb**EBx`nVCut9zmz9ER& z5)Pl#9IxUC9jp*a2S#oe!dk&P`SE!!wDm$mX}R!Cb>j7I%y#2py3#e@@DBDn40h9dR7?-;z7R9 zB0npz<0Zv7)6%A;sI+;WcLHEPfqVjBUlo?)i@csh+qj;lV*|&e;Z@Yyj<>p=*x#}p zUXHhUzd&zjw6N_wi*_xLGdQLJk^{}rHaC1jEg0Pkd3*RVm>kY>s&liorG!Bt7}jQF z=454h91MS!YRmJ=0O~{8{s;OK<}a+sQpYN1&K)~Wl>+OgX&@ucN08?h=l9Cn&Z`35 z#dz|Km{NUzBqsEeWIRIJFo&bX<0iwkStrwXo1ZzLNZz}WOLp?V*YvJ4a}?{=Pk}n1 zB1O7t3HH6D6zB3;G4$v+o*t}fc^}5zcFEI0SE!f1T2GkI`zbo+F49l!%#u|tL#L>B z`;WoZ;>x)cC0OcLvi7ckE@p)8;^UP_s(MX zo@f^15pGjSGiU`}qNpbaeYECsNL5QAUu~a&%JImcjIi{oE1OkupShv9IHkDfDj4g` zedd$sLsn;>&qPSJl_9iUU8{Q%Z|IXkF}1dnUNU-&KDSfXFgu{ZXR!oPWCte{l^RX5 z3{mGN?rd7iZxT5xz6{bpQeN#|s7pr@Ywl#W`-JLIJZgrHsBSXEkgi#Q5=G#WfG}`m zdMGe;aYa1o3iWp=+<=8-b>(dCDi%gAf(~KA)kJtl7O|XdymELGI!bF zpllk%!_J7Tz={<>p$5sF2eXs{{7fO+wm+Q4;D(A(aD_@3HsuSY>0GxwNU=(rDF!iJ zJ7!6m$Vx)2%qTOk_CGkjb6k$DMDzKH@OquIQzs9?Jr zNa`13(WBW#YRa`Xf}}6Y5qz3-IPh`<5 zCTorLZW7L;-YM|(K;hL9hW7DmENvBt%xAU*O{buzJyd7lclExLA3}*Oj?M36A{xDP zwv)}ljPIgcA9|3nZo5NB9Nt|ww$T4FR;W+&3(Q>*!V5D+;6&?p6uJBXsn@6)svEj$ z{^+0${DBoYLkL$Dn>{x8t}(|g>R!fe+Oe3D>aqK)pNHh29D>wB-)Xgn*`!`Fnxdn1 zba>1zg)kzL{HbEDPXWB)n*|{nVZTz+0BGFQ-H6!;3C4w>Qg|b&K3Y=k62DRss71Hm zL}8f`LB;0x`JFG&VP7BiNmC_mQ-pWWzX}H z_g|;QTz~w2ruq&jK;Piw|A(3Oe`zZJ;DIPAO8rx#ElOTm_7^>z*8mMwHJN`Y7-9e` z{C+vsiY@)^BA#7RK#M>0@`d%7H`-S+DI)CW51+(3BW8S&sH{fD`=iIHLN5;=HxS#v zDvER(%SujD_CI(^GDcJlJb4`>S1&+54+J!Vv~Xq=$``cyD=C(}gu5;xz;9BU5CU0a zYEXBkRPmHNFwFCY=KTZkvV~*3{G1&LBPKY9!2jE2D1@Of}K;e60tU!}) zV5^%DzA1zF$-yB#%*TWqFB16Uq=z{4OmjP=x_!BNg4PD;L!h8gQOWcT#s?oFK~<`$ zHN}#qzH#l``NGjEvtm+Ug*A(&R^9e;hP@}FW;dv)I@vSt)>{VzX{{s}f5X~xb{4gE zIX}p3J%>AFWFMTf+(UwNIv8TN0BH4nW<(xwl@qfDpV=B0_nJ+z$^r`Eb{VxwYn9q{ z7Du+bN&z|VO4>qs=hzNj7OzxD3a$AaRbwpisRW5m-i$18;vhXx@3Rec!TwQBh7VWY zEBti-f(Owi_)Hp9oy6-gQ3eK5=JO#`Dl{l4(?;gM{(^i(!?V%`NGOAqvnVl5yx^aB zGv%n1`anMT19$bSJnp3iF&T?ckQm3wV+2y!BUW(}DS;a1=xHV5!bOmW!p60}eGfSK zZHdJHP34Lsnv7Dr|0kg8Kd3{iTB$EM-+vA8oe9wX&p+p2ED*(U4*&dGo2@ zbo)$df0Q93K=Sy`>;l29B>aG9R$v{S-`Hn-+G01U{eEjO!uUDC;FhQa zuMW~OrSzyQ2;8ey$MXTAP$o6DKVTBK> zVr|88bU|-|{OD^ptvR7)jH$*nBytnHd^R~q8{-0TpmEp_Hrv5$DbK~vyua4-)xOe= z5NZSNKix6`rrB%Mf?h)c=KfM?PhaRX)eEdSdANP~q!F1S2NI$TBDV+-Wd6HU03b{{ zaGQLgb2t4h#U+d(iF28ya^vz<^D9QR3NNSvWzOIiaaF51jr?f76!*v9yhFkd@+8K! zr6VBvx$^cr3uN+orb_tJi=`6bR8ceq#Z9WVL2@bu9C=vExZSX35~$ziM)tp(nxd4F zJ;3@q;kS4EPkH!%y5*ptA!K3rhZ9k*-)JiV*~{N}yn*>DUMR^u?dVMi4+tmZdEzHcwnJqReiCI5MdbWO ze*+jsN-5LDY;N9Vi{)FyQgnVmEi^ARA7$O~Yp$r7hxUj=q@fsT+VNVQW=IOBR z1ItLg#MWv9T^mE-6|EhaAIOkJ zqnvpqJg(E>pk*?R2~nejELVqSv;#%SRswx7m5NlcAdXY6R>1zZIU1u5ouzm)WBb2d z^FL0#v6n)K`}=bH{{Ak+q|l0pR-u>OX!!k&2a~rZAGXv%?s4 zN|1o7SzQ%qI1>?{Z?~F2)vv><;FNUV=Cw28wm{4T55_juq)#N?F)0%=>^FXF@hpyl zeXtPpukf$F+Y4!Mh5Vh=w2M`4kL-pS9ut@2&(GtDoS%_++o27udAnG{nD8Tk!5OZq zuhwO*vH`gJKRanJ3oVP8r1qOLv*Wtdqud=Rjt=$sSzLZS>Law^JPo zfsz=r4TrMyYX>3h8iSKMco21`s?l6ko2O1warW=~76`~}{}z-butL?BSq(>aWW;rV zu%=JelBz0{Xg@@{jF;?-8XHOOFl#kV_hpryaI~Sb~j4m`iVF1RY2VG((p> z>~EKsI6Ze0L}*%Y_m-wjvwt5i6U&F1N-)ybg9q7cl7!OltB;&_)N#QIC^jIqJLU8* z;SQ_ZrHSWhH{t*Zj=tgJP7Cz@O~|mnyU$;83$^X;yHO-Yk4h zmeIT)PtFNyHLY7{<&_4QHK&gbCz3X_Y>WBg4}4gOcWz5$F_xCSC!~Ou^z9YyB!?4_ z7%K0@;8D1(>*d}+Ws8A?(yl4VqOh?P+k#H0D0_+RC4E-f`AI;51K}Aw1JZsQ>>Hif ztE^>UT2tWP%Q6289j@YNol&vE;Wm~C6?X0?l$D%%l+=b~cOC&{Ex00H2rXLhr{X2L z2J8!M&2c0XxQ~2$b2&H)^vGO8V-*H7sID4?DoC<6#9cH6y|PL25d!4c%Nq0B`o&;e zQMm#=Q`=wjIx!Pfm*3mCJTMg=2p=4NR2+x_P7*#?^3>qFzpA}z?=(qIgxWcc^HN!h zF9rBhES#1#BXp8|_s@t~lX3~mPEVP@msuGEV#K${Z2UVRz*6PI+Uy{tE|)9eKvF`{ z+yQ&ccfYt8p1D1d$vjM-rzP#f)=faJGouvZ>$!Yg3< z7xDc`hYuQ*!ZdMpR4G?^wh@fqU~V{IJ#>5&B3(8_PjY>hV6HC@Lc`ham>g#q8T=i- zN*7SNX2Cg%z%_|rSyHT7mOC)9(iV|iauUUQ(Z9WUBe`)+Jc6t1F(DUCvwd&keaLP_ zDjp*ZV=`)7C#X5G8)o8i6J=6yH<=&Rempne0e9Giwx1Gv@Zs7^OE@qvdIx_ndi3%G z=k|BA8t{TaK>{Z|;8geB9zR9>(Tl<&0v>WxUbO-o(B{r}XrM`HV5 zW59oVBPkmbGwXljl9CiJWe~r!#fC<04b)hI=b;i5>O%u~cwsof42fkVI`X>^UCFdW z`ps2im-3r+)LUdJp)g+0AGqVLCUm2!3Rd%JoX1-ZS6Ln=KA*3jK)qDS3nznWq*q3< z!yi!gXfr!Mag1X}0{_c%Q6sGn zYFmAR{!M!euz(sHKT2=e&4p}W!C_AwFu=Tr$=2%|Rs@@=J=tL=JKp$e@dvbHp9*-c z1l`BdJnjT`P4E9HL3&)6V0cJ73Y~pqnaPE`%$Zu^#^n*UAlFc-AfEA%OqL6Q%5tg> z50U7J@}o57tWz{Z>!DK6s!B#Zny44KX3AH)0%PK~o;Eyi2@htUFE&4urSdlpNa@GB zA%^BDE5qRq4IPmNJpv?l_cD2vGC6Vs*04`0UAreIOg2pNzp|NwM z|2S!Mj6qwVi=TsFS%7o}vI{LCs6Sx#{oE)trT!b2kJ#7|)Q?(QW$D!D)~ zoz!{g9L^AQfwjEs-AMtr^qXV(R)#0Gs;)2&b4vvAcq4Jt9#ZTyS_v_57z!pswKaBsP^WruTaan`uCE#W@f z>u~2t1x^W@V2xa6Cr3QWs2aB0Z@9EiIi~WBMru`l5&mQ;kadUfVXuXrS)e6dWOXH> zIgv8dJ9H}Ykt13aPaxpHdAn(2!F`URET5G0W9ZSL-6^T#bWuZ6+`2tb{NZ7 zh*ROMFD@^(U!d9yakNb#NoGlmgM7gjCkbi}f4`_U+ggwmeuK8toPsn9q6FP#cbR|Lwr$(CZQHhO z+qP}n?5Zwxxuz!OWp-yaVqb2=jmVdbxHt2h%yYgkWihAIv_zBD(Y2&dt7NAci^Lwb zZL*(PyZB#8N-kxlFoSDzUg(R&qn%jvBNOqCNPinM83gMc+nIZ=9mcw4tXJV zT6kZ9;!tF-%lTZ*piF6B@F)l?^V)-BQrR1(3!(I3)Zmh`aEZAEf*$_5M0<7$1stc16Uo=wmDDmH1S053q}c`$xH!w&j^p`-7{Y8fN0QHq2Nog#8sfY zA$(5rxWr|_x_=&b zc_r#DSJl;=Y8%a!>gG>P*U3uAi#chy7zW`FR4Gy?*s#oBF&T^A6HGW2vxN3t4r_na zg6&cw+$SBW{jv!%EGMM(4Ce3uBqA?o&8N>V_wEl!3QKLjZQQwmmKdMaJ0c4#`HW*rz;zC?L! zO{>b3C9-(WIZ_&JEr5EcJCxKFep#nIUn}wVZOvjP+mXgG?u@n zUX7FAD@Z3y+F$8vWH$v$f6*NanDCF;UK6YrbVgNbyOA8}zXDhx-*YD{0Gs|}G*pVP}PI$)yg-mReq8swuo!1Ovzb&7>}l?~(~ zDrTZiUYHJIxPra5Zd)7tLB6rSI^9|<#9y$Rc;bYUvf5hgF9^cH$&3u70s zzU(LXDhW2OMpFqiG*j1&xYfy0`H!o_WUZ3$cC+l4tL+%gob+PX_12w$JAA@*LLitlxU*&De&6}-B}mv3EDP{eRy zb&d^ZwoHhqJM>#H0XwQ`!%Uo{S!RB=Y*^lmTa24OOt+0?ACHMK{5R4q;h>)oL|+)K zFP_5}*Ww3I=&MlniJyM#cSO|(BB&p>FvIShGgG7q@n0c?4aV5`VUP=Q2?D`g!N|!{ zf|#X8)APC7xM!l^%duZEK>>6c*szjQ7287R1st*u#iRedZt_19S#x33oG^}GzfPV1 zm;0*!naKWE>RRIs;jOH)`qFWdBCQJz1q2KP4vVm8jzo_FMZixFB~C6{4?6)Wx|l9O zu8Oj{%axDcxOpXgtnU!||1v(^ytnS`<=okX3i(~`E7cSF8xJFb3>BU zCw=7|viWU$rzZ0SAN0L+T<4+B;kUW}O*pN~_6i8T@4NU*%ClSeWe51cfp5NZlljV> z*kgNzSbi&j0RAAV^pTE}JO4`&^BT?TEnBCT{vl=g8i}i?G{pw~y?VU2@cs|CJ^q`2 z>04aSm+@r|w>|sY{9dp8uiE89*1-4CLEpnSk^y`MaDAEn<9;bZcw;b=x`|4wqGSTUy-aF`Uw%_y8f zxxsx2=aX@|DILRA;ufy|SNMbqpLK|+#BCYw6S_W^zt-HZ=vUt1y}4ePdTR0&ddlBY z*TFE}PibR*2N8AUT@cSVjBg^FLpMKPAt$MEv`8BRm9uLSRJSYVGj~_>7*qbhLXD%a zfep({ozsusJF0hORbiF8gkf#kFNtX$Aa7!VxhglBFoh+wZoT|2M6jnA%pg9X&QrLH zUJgJsH-Clze9RC{f;@dWcEJQjKddZ+ezGoFL~VmaUc;}G8I;t~$}2^PD*}RupLqKS zP%I+gnaf$!AWHB@)h`3RR6_2D5`*w|vqsq@&(T&0|pq?&eqEZtu#Gbg>I+Tk+YsixQ5 zOX!-b*IJ5VQ8JGm7BN(^jxNIq>n8a^hUjVt1&)4CjCw-&%5g^1s5AkmA-%s6Icbk! zR%V{wFA>Iq^ZTGS6gL!gVy*-dn}a`6X2gR%Oj6ht=HrF}YtcaZ=HOltBh9TXV)wDl zu^Avbb{fiqJ4Y?`cBGAMLTE6^4QEom_9gx7Yx$O!!?pWE6ppn0oTTat+8e+Di2$rD zke1)iOIU#V31s!nd5p(2FmZ83^pQk54emB;PLN)6%0srk!{Epb7%GGy!|dXXwfXLT zwY5P1Lm|uO#Zc10EGM?EH=B9+18NsHcP&%YgJKgzp1k1(D^$m6}kI{?l>_sG#tXkVJdd~wx1LKScm zz8gH*TRGZ#NHTSab%Qk^lBXTFODLQvwoJB#B=@_4`#ROBsne71J2<^J&7Jg8f|`Se z(_8q$x=r{w8f;$F;seQ1Spty`?|D?*r;!uxOQk$r(?y4ZzD+rSjP$>o65PeCiV817GO-*vi z;KJ>iA4TNR(UZ(^UKbpIf&NFRo~*Y}3D63!P29zVW#>jS24)DIN6-W@u!5f*g=4WXn*{FtN#s|zj0uC9VJwA)QJY|S}PD(G;Ix&`rk#Q=oBEEa^ z@AazSKqYQG>PZg`v;|-{*RizzLcwIGp|SF;hd+m`y@f00;FL;H1{%X?kW*u_;+D9$ ze3kTe7Va;mdM1mv84rhOrY=KdW=%AYYqCH!!Wx_pT#_C$x8|y%Lst@pTN}Ep3$Q>w z)p8`-><_m(JjIVdU3J2)P~NzNoUk{N27@ly=#229*da@Wx}lqU#NP+ZH(FHYVP4*W z$H;tvZ|xVk#h7$U@Ub*M5HIy;Gx9)Wk^g=`UcjgUpNW1^O77ab6H(EJR|5u`S%9V@}lr8NP^Ir0Yp8DjfYka~2>CS$K=)NPLk(6gt?#f2_=~XgYyNtPtM>has61WKb>c8iPF&JeDsq zV8__ifW0UVEdUJ1XmlQ9M^;I`FphuQ-u7pZA$|J;gz=7ZBFImMj$Cz>%{{8Lv5r*z zEC*k1O$=S)9ccXbuXV@Bu9=;dHP3qx0dCu{9(!cOKkH`tiS(K5HjCuF9 znV??M29o%6tLqm5*PqsP%|S{IJ&mc;PI(L@uDnep&dr>g0IVn-e!Xe$NG68j*7YAi z&O4q+Gps0Hh2L5Zc`;=jZ)8FPYb$gtGx>9E28k|lV6SmR+6a~KU7yx9_Tgp73qoC^ z8MP~V7l4GGFHutC5D(i&HgM-qxFz(ZY~+htw`sU9yvaI&L{}|E3FDE^n$@fUupxuC zHM6T){Y+37oa;u7p)}vV|C$Y6nO$RykekdJglHrOM#+3r&NRKX+LK zp{w}=7o2(JvIts7@MROhrU4ir#M1a7F}IG0F?ilBwBxr99Y%0Z9HKojQk|jBg_ z9X?bz5Fc<4@~E{3u~AF!lF-cPI^XGz@m8wmn}2m@ghQexO`6V;*Yx|(!IeM`#lMxB z+Bp@iwP=M_^7ZdXdHk{)YlP{6UF@346F1tT{`-5&EW$0~{DUj6rR=hw?iEH#B>9X$B~8#0bky73=k z-twZyNy!Nb+Iz^SWdxmszSqK#R2+A%U4ztyN$QKFx6NNfWulhFC zkcwTBFH1bxBb(|c&Wg-7@EGvWcVSUG;-HpwXibf9Ts)>^|AU4!HOvTqoo@Yb9T=?$ zV2a*`;kHArs2&mW(oykhhHl3m4>cMg^+>Pa-XRX)$DR>dh0|^;M*VO+>c2%V6NY$5 zF((lZQfC_8oRlDRCUf{^+sOrS28Pwp`4RZO-`3%J)X%kp3$Z{wBgr{lHzgX^o~}V7 zwWe$8*P5<4vJ7pF#<*yxO=fdYbX1%|ZNqiJQjTZai%5MD-|B~MfAvr{h{&2#*@M9~ z1C24l<&9Ygw-T{wl2DAjl2qDtlh%X@;_f{Z6LYVmyb1gU8d8^wZ5?8+Qkjb_CfJ)@ z7m1D*qV@G77<8R-9r4Z+A0)9V0TJiz)JTF-nv4VS5$26oLr@pt*2k-eOR7?&h_e%o z5JAyHCsnD^1kb`5-I~x+;%CW(JCjyqeVK=J`X37J^Gdy80dnOM&KZWb+X$t`q zbd)p?vUyxx-SdY|6HHMSDX6KBNRUi;O4HscFjl)ABoCw-jtDJ9R&ZrqXnYyq7rH00 z*d1KnozwVJJ(4cv0??SPn277giAat;2>~IxQol=IksmA&zhb)s2_8!Gr&7k^4b@+Z zk7N$GJMz3()qeOm=hCJ~{q8OE_M|qmG`UQ3Tz0Mmsy(PpXDPCEgDElLHpSRn3Kq90 zozkci{RmROQ-&~(ab2y^0q7(xQVVjRImWY5md9e1kK-*jQtdh9NhD!zSRMU%u*=}{ zXs9mP2&A1{?ug6#<%p3$&!Jgf%E{`foZgN;bg?IRUtzif6C{LTC=AwqakNUv2~9Mu z2{dfRu_R6=xT63ZLUj~gQpE{K%7^Ac50Um$B0Nzf{YU1ibxWz({NeK7MB*HZv5d#& zusg>XQE^HF;tobqIx&u?v#q&nypZ{KQa81*PD`a6?V;j9d)W`5m5S=vsu? zD;@OrPwE`2iD1Ulh4?C6XE(4EvjM_b#wVS_peiFl9y$)y1C-+gkRhzV2ERax%3Pjd zx0gyd6FLCFBx4b4oCnp3B&eM1V&Jy;8aW}p5p||?48;06LW*GPGQnCpmq1l!bhP*?DX-!nr95ZOH zCyL4+S~j)7zfy8vZikKXpGL2nyA)+LU*Tw3rM_G_^ibouAg@ z7pd6;WgHp(2G8^KgWQ)m%*4q6u0%#Itq4IYeRMXpjcATk7)e2qFjs?QGP6lV<=%+Y zY!MXwBpMH3l|(M0&|{~>&VZajH#@P>Go90_QFj-05hEd81rrC0iZt0VA9f{#nCcmX zG(gMxjCIh6tw9^1vN3k&E%=Gx!^HE6n8_R3zXwgJ)P(A&ij|df#=!&ajJ_Wc;WS8G zy~aMMNGypPOIP5A$5dcU)pgNbv22$#EUfg~br+IDbBK!c7m?%Ts)hG5tA?1BGyN^L z-PTx{6ZYq5`N-PxaT(9ncG2cmbhbv;OpJl)(@mvHGX*am6$a#ywGseu_0bD{Hovn$ zKZZ}#_0IGvdv;4ER#(F44e(!OjJck--bB95S=pf_$q^8%?aS8})k!ZvrpMyll?m98 zn&q#^nQt+1Un6bi(HIFUNYOnLj}h`Td*w08Ip~t5>&*SL^b#w@95Jh9Y}{9!1Ro?~ z3A>n|U9nv{yOBOB{oyxLqR0s$u6_!W!Uq0nxdy5bY1NOQ(j!b_b$JRzJ3wBMuAr*P z!St3s|5X{wjdfP!4j$!0{Bf}3=9ZV&>th3Az7%tUz|e0y=^eFb`*mqEGJ|!63)~pVn1FVfd&75eK3$I7Octl* z3Ew>WDx9~lK56-XBHriUid>+}9IR^G#c|Ib_&IiA*lB5KQjVnM@0ch`D~mcLCh%S+ zDWiWfirZTj^}5=rhf(#OCn9Y~WlOy} zTV2su+zeEO{XxX6+`jlQgwr+#f5|{;Z#Ry1W+FH`d9aa!rX}pgtf;N+$0O4S_$fRr zZ>gy^2-N8E^v#i|Uw?D8Oi~Z0vMZ z5`pk>pgAuRzq@wW+-o!1hJuKKzc$eQah9`3CYk?hs?4)6>n%Z2guH;bLto=HzOzFx zCzxf0=BSpsV248JOP+D+mLEa*%0`m~B~ySGhw(UhX0{!zRWapfnOCVbFsr<$c5S7c zbJ)+ldgPn)(7Wo+yG=%CKjAFXp8Lw5gvt3r=QtVpy_;6<>vJ(Jp7OIGDtnMT8(mXg zg77r#tz2WRd~3SqP}Z5TmEViHBB$a`Dl}XRWqGjMBG2x$P}hT(SMhwLgfDS7@0Y~BXZKW!DL zi@fH>P9kL++|TZr2XH-0NZ1ASdb~(ogvGIrBXkw`1xP0W5Uk;!2?gy!*v>B_58O z2mE#L8@mEA%!A?~15f{K4s0=u(PE$!4e7gu85uf{JG)^Z6%EO|g&DjN#jB7!58hQ? zyN=gZCwdWtRd%GPZ0e2|S^An3obppFiv%Q?7F)&b{x)jF3r=>XnZeirCsOW!5QSDx zuMGn@6dG&Kmxo-zt$LnsTsz~O?SK_=VffO*4wEt1(yGftQP9JC4pGDdwa~)?$HSQQ zo42Dt?=z7bPPsIetdOfd_81iLcNQgP7OAVJMTxR=sH?#?ts# zaAm@!RIuK_ACoPCAq~m(ymg=UOabSpel)3J0m=qn$P1R!U(ir!Hul#w^xS|dqn(cd zw6TlU&$;imalcLZ>=umb7+gcl1!fed4z0o@w>sq(Ym zC5~JN0_oRxHq4GcR_k*hxj}YzNTve{bAPjV__;yO7b?dqk9IWOeg?Xu@-%()ZX|{C zpAIqC#&^V1+TvM|YXPuNg0Rb=b=uX_bm(ju1=+b-fghW8gkKSXhmydJ$-l^SsGOE0 zL?QLA4P0!Y`ckfXZqO_DPrfwmdS$A*enE`077v9dSY9%7&uy5Y3_@ZooH^uWG?sIQo4Lv_#| zQ=+7Ti(-aJ%r(do8L@)a&O(b1*onN8&m8rl`&=_c1cpJaV*D49MxI>=3xwgvE&Z5N#W76l~2@g2Jx#1Dx@w4H{ z!@|cC>*26Jqp-1^4B5i9_wgO^NePEIi5l5S2_cM#7^|5Gr|hNk0m7>s;oPVSuxx}Q zT$}`rl%#}mO=gC$xvL(i=))M)8%@`Bt%Ffo3M32SfA9Y|R}~MTqnq!zWd6B-^>oWI z6>e^x7iarz$b5$%P0M^2(V&2aM#ybR`YgS)na^y3wla^GS{XoAd;!2`fG>)7_CR26 z;0f9TAL`7io6e294ZW9GUKoalN^<<|4`K7t01%L?AV&sU_T+9YsL7H+l#~_anwD51 zx1O*KLf+E14ahLH9p<0#ts%bI2>(=YW$RzRPWG^&_X827urZ#wMugEK82ZXJt(z@e zDWM)l4ZpE?6Ro$0vn!qgxr@6_PO|52H7--8;FsGZ3avmSJA!00Ks!iWjG@PoTZf6N zT8wdJFQKR9sfCl>`Gv0FvH5B533!3#*|^AMRc}PRN0}C~>l9EyCWl|BAO)^_n%70G zaewyD8}c2ejFyYzD|ZhXYHJT$3k?es%gw{hOG=~N(!X%sAP9~tbOzpn6r!Kc2`OCf z|KpDUeSj5>vD&EG4x_RU)QW5rGb5=*@bnYX0kC2YRy05KF0Pg#lJ`cFXoIbN>*Q6zi|GVe)CKS~LJiufRYO?u zwD-=(USKaL&8XEI`jJ+uB@-^~mOA-8aL>+OcpGC^RxS_p>M=cY^jL#oRMC%tu@TqN z(738w2G>IT9uT}CGjc{f{qvpUdte0DBrf<5g7(Bxo;Pu}G8$p6b zUcs}th~Qol!%b`=NbeHFP2b_T*%2tUQOdYnk3A?z5JdZQ8mJQ z7{S_Qtz#BT@)x8{Ya4*_$Z}^y6*&>kFRcpC%FVI;`oNvpUEvW_Ne35ejaksee>Bgn zN7Y;MClt8s8SZ|+PLbc?`4M|~B%j3O{6K39oDFlv5i4ETw59NbWySC;4T&KzazZr; zGEhu$F34G2g$Z8~;zkKUU>ER^Mt+^sb;nbI5B()XE{!0NrwkR$zyN;`MpKT+7H?j} zqqzHB8)g`qL;k4SSocYrfj&3)O}D_UL15Mtd*Pb5eNc-j{)Oj+$3T%8cEY@HNu<;+OGXd2 z#DFVd!$=rX7e@d$vmNQjW5AC^9_0&42P2OdYwr=?MH~@b0YE*-Ux8YBNuJ-dLsOUb zBH5{YkhaWAN)GUWqFC4K;oh1D+Zju$08Zj6L+VVEnRg8lxNY-Cs)UQ+^!4k~FooEZ zh@OV2zthn@|85{Ns#wrf|3b1i7-_G!7;i>8C!owHOSLfMbRUs6)~AR@YRc!&{)H@z zDUEtU|6^~YriJ5Kf*`MKZeOI00tmDD;3#}xc_Fhd*{~Y2ix|*j&L`81i$RAf+7g9Q z%Gv@~>A}0Mq0Jw=ZbeTxHuO{%MKVPPHqhgmMFb_#1GOG{6;P55qL8Z=?^FVx5o;d+ zYad7;kjVxZJ5Ux!Eg9tB36!e;CKAS}tw$VG6Ovr6>}@l~NF*0IZdR*wWuuys#=D$_igV zJBqmTH+cA!L47-7`F6sP6=GY$aHRL;bWB+{hAIhe)#}3u9 zmzSQD=x~F#KC1^(J-rU#V<_(z_tnsy^s%`!gT7L7)i(j5))!yosZaW0@*hv%?)?gK ztUmz)fANkVz+ET7Co7>T@qxbt|8dk-SFHe|Sn}0(7heV#cD+Cne5Vs4hNAVPON+0*U{6+_ew(#yfhmm>j+qJb$h`bjzcK@y*ouV&Blj zp^7^77gSk9n?Zy-oND-*L5Vx^YB<|IHT4|g?V!maPrE=jeAY08ecB7c#2sR@(I?_x zl56z5K_hiu0^TsP5w*w}ne*XULDG*<)*1fpv-FjSRxOK`LxXz|E4>AVG9JuSs=!5Imd`vK$2(YzuS7>U%g4Xw1`Ox zd-AByDl(g(M@*jm1x{op#AMb1?a{{Yr>TaHRK+IbRFR0z-?)cETq=Q?-n%(rxhY_c zPxDDN`Y4;CXrKD)Mle$oN+29e>A zDWNyCZS!xpa!X;e&}F^w4F!H1*X&stRB8)*=(r*s9auCTL^BRO1w?uW+psb0K6%kr z!NTWZv0#Y|r3`SzXK=`4Q*%S1mib*H=5%;33=6G#v z<`lQ*Kz*$PdYThiJO*oA8qOI(%~-{7eN3 ze<1n@p?!uhxHkd>JgCkoASRofaPT-g_zoQa+^y)p4d0%WQBUx0?wq-}gG3GKxTB7~ zJ7IE@z&I;DA5=m%f!ZToxetW{c0JFdoRzPPX10K?Zi}r%0UtH;Qfueu`ys$YCnJJP zGwZNE=gI=PvUoGjGQ|dMY0xZMr}x_pYg5VnR>|c(f-FgvesRYS>kl2yGrMB>c6GGY zJj!CB;%(>#;>NUZ-pu}#8|~XSkjKKs*Tm;!abZA?(AY##_)@YVh>lp3hp^@GCH95c zU{NPf`$;D9L{kP8*>EzBzk`n>>{GL0u1rX@sm}&b9eKB@(fe=iiQC0g2c{emz46h9 z#_myG1ar|E5PbOWjE_;np7xz)$!SF?ZUSsj_;-hulw)NKcqckzGoQqAC%p*us`fo| zLqEm8{9^9D)Uy83|Jt?3HyeDE_si=m{KUB*&yjD30&#(BWC~F{zO0-BMj!eJnK<_vL>49F|JZNo7 zlR!YE%uZesFZW`P89B7dj`Kbv=RQ@kUCscN0l~M6@K*m{dN1-o^63rwu57;CEvK0I zGYVM+MQt3%h*wz7;b%F;>FG@YvTd_3`*Y3ZZHT*^ScxiTLLEV9r-j4^G z-6z4JYv`kIAw@X@-FXUELZB=h0Q&w*L_7igez%*|G}7axq9+ODLG5Z9T&dx4?u=e9 zRReo=t`oRp_bSxihskp6C-CaFqxWQ8$3i)d=}M*>pD*^Fa|9^2qhPR`;k!{Z8!&vz zC0Z9y7K_gTd}vq~n*w*mM*ho_kxML_M_(2OQ~d!B#>QZofTX-*mAMt12>XuT%ty{+*l3{4(7Phjl zGp7y>TXksYi9H#r@hH1W;v0huoD;Hg+aaaAG#2R6SN&`}hz&dFx-$oBh#KE-?yGO_ zK#2>WlDpRpDB{)Avq}@&Eak{Q4eMpmv7PyUJI|a~a0MHX!BjIvX4HtWUG2o^*q!#F?L}k6P;1qC5%BHW{@GzfEDg9fxEJ zv;*-(Rak12vJBRR|HPpfrEQ9fh&a2x}mT|g*NkeA;;=#!v9Swg@|80`l%_u#6ZbPN#5*fy$U z>VX&%tJS+EB)1P!+BFsqTj4~T2ZfozZD5ZcnMsl?t2h@n)>U84`v#3C#ghJ^K99@B zsoh%?c<(M5(IKl;NSBJqY|b|Oa0x7lT8vqd)N5xIx;$RMC_z zK8JDxXhnp+flq#`=uxzbK6-{rWLp<;dgf22ft<$dl@fUS7bf zlkEnnc#PUx(=K4Nz0zK5R2_cn$y_5GHZpCe0~)>x8y%0?RCTR@*P zQpbCj6W9M~!rPixY%hA=E7zdkmOY(y|_B zz&7o5t4{7jY0~n_P9CCK6h0_znC+HgPvEVLUXL_^{4@b$dh??XYAr`E08N5#?X2Ox z1=5Gx7K*|7QfMhEv73$EUHrT6vNV1{_J8Jl&I#UVZ|EB z?1&T18)Ju#R&ncrds>?YcWo{m>}BsmjvCPqrH81t7L!9J8vNd+%VED3Ps}owNnFjQ z>!Ox%gH1^eMJ*CCi>ea}byX{~iG0^qZYeG?-Rc~wHUp15*IsYoE`HqQ)d{ucr+Y7I zzZKe)u;wXdoi1&u)!OAy^EC^O%_njwHp`=yEO}mA(TsLvl0(zvL>Y3jHVf2EnoVq* zrdIi_!kk(g5seOHk`q^(64x--T35E%HzE;2>9F1p1B?j>gXH*eyfABCjG%ks7aXGq zyNx*ghTiN7b3&lqvm|9k1UlJ}0ZYGeq$|P}5&Eew$}#+IBjp=yjEdgTDYJS}FR8zq z*y5&r2CA9mGycxfc&dVdsUR-g$Yy)=0Ao6cQz?)1ihLe_$J^xl$wkcEyJ~Y*9efnmPDf zNzi!t3whibmxn`thauCcZ>e2y^7YR2I^p0gUix4m~R;9U_b+6E%%Rzv2V@dW_&p7G_ppb~%95v}^}C zHn!MY2mIMKJm#hmy|C9d%bEOIwsW;(N2ZIh@oJX}6~BnWYzNu)+P!YNs|gowLDh3!+rL2j0ro4CDHt6>S2qyA@v(DcG-6> z>1EwuiA^c_;vU$O&8mc)U8gPfPh+A-M2z4P^TCp^Qr45F@S)CBx>VngN91-mrXVE0%EFBV&m>`XuvuI#4LpSfK{4 zccGn@9va7zrzQtdkm^cl2!&vD)vV?*#aJ9+a>amNNeyvUQq!#z0cUu{7D7D7!WVjV z33RDQanmt@+{6kA$zoE9h|afgBo+ebX9nAKADs18cV|;~6fUNnBXDO-bP|=KLQ`xc zFVQyrlQgjz7Uos$bv8C@c4^jd6Vc*R<@1_?`klDO*0+|g0vK_NEHJ3Xo!$QbfJvoc63mp0M3 zWE|6F?Vd!7yUun^6@rk#w4r9xdA2k1@Y3jgZ~*nimf%W$aZv6I#S<%9!cI011;VVF5Vy>pD*LjFO@PiIdp78%^ugzH#!d1Q(aS80OBWrxI`A12pma82H&TuDkq zOQ&G&ivYV&Y1>=l)$HFmDlaB=z5q-kQ)-d?f_iD;$V9BfmrxtU+ySVSSz;A}zYL>U zrgOZ)*V!up_FXP4Pd-t5my~m}s4KXSANT39-?S=k9#pot4N>867QjDk$^Pv~>(5Ml zhABQPD!v#*FAiV8%{)Zh16F!TWzgCSeR@%45YY>sdTA!u44 z=SbyR!Ys&61mi_DIKYkrHabkreNPq^dpPQqS*)m+Enat*aCpBf}A2)
qD<_!X=FCV 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�HPV0olP9N0F^UKGV_jgA_82YF-d+kud%>Cf!B8d8uNnRYBu zxt|)u$6`=4pw(){PyL$OQfvaaEs5mJmc%UO9?L!XSWxza`v z&v}~t(NL}<`T|JOU```burIf1M)AG!B}-~&H!b8B?a$#{Z{p)gen?N;-Lz<5V9h>X zn=8{34X}RSmtXltd6;Zd#Tn$SVniB?LeaK~0X=#Rn>lVUdZmObkKL^vbJg%|f6Gpc$47J|_>h;5y5aBReH^KuM-Hs|PgXBi zDXl-p78q3eUt7Bwn#R<)NbDS|o)Etv&(l=dEU*p`>lu+!>2uUpiu$r3T~i8jjJNC> zE?OLk`on46<(0ywBk7^s5+Hx3VU_hOSfy3UOxRgcE>~x+tnH{wnuD6Rf};TP$MqT zV}&=i0RA9sO`46Aj2U579{v%ux}exA5Jb?UWC+7!B6L^M`fY7xCB=>CQ892=3piSE zoZ59bqT*7o0hX>4qGME!m@_Id6@p$Wj$a)}zST+)2zSDr28(b|#leUL1N9+3&g}6A zJg~rwU=mXMjz`n2+3$mz-&A`XMk$J&MwOO1T7zs2A4e*)Z~h#1X+C&)Yq^;E+aP{g znO|0ICbT6)I9W<4&ra5v089?C?XR&}kGtB?$crIPH`Gk3s09bS+@c+8D{k=IygbU8 zeYBZ9#@bCOCZdINj6J;Yp#W001Pdqw#bWaJq-hNzEw}8OK>LA+rfz;f%vO%@#Ce)C zS9I&3GIiL>`0#k(oV_KqA9E$F!jk>pNky7VuTs1|c7Vl0`q zT%$JC7E#j{);mqxmY}@A?;j}~-b|&IhD3_Og_F*QpE<2%+E8=u)IgE>Ks6iX+GZX7 z*NP!z=NEQ0{Z-zQqs0TmVuVn?HFLC$p78d?f*!@Gy z{mV=!xnB{C1qJX)+5krznIqt}5aSQRNIUOE94x)xuV(=5;aqyrBKPpmP20vTg|G&e zTVjPQkFvnyH>$U67*I=9Ace(7*5z>0MaZ`hPcT>ib{BBdYC8+K54*v4ipjlfo{ukjYh&M^Z}Ue* z5|BcpjWgUO7u?RZE&QbouAFM{6i)BiXQc~>!?xx{$7(!&87fwg@v=f=wt*k=2Eqkk zy*3)b4C)5@u1P`{jOa$cAi?;oYI@ikyVBMNKWs*>(z>+fKE6PB;1FLFFc27!f2{j> zJC5%AK!bpIpn!nT{0}$(KQDbL7e{xee`@GlExoT)AKVX3U0-|eD{95dp|~7MMLW7Q z^?4&I3a-d4ZWfK4HpN_CnigAYGDlk5AIr4%J|vJ}slGnJoHE9|{%3NTS&DQCx9ny< zxHtf=vOckVBd@*2K5(Y6-)+-WX*@xx9nDXX(J>=il7g+;4HwgGQfx+@6k7zm{W@Xi5NDb=EfGqL52;M8rG|W zZHBv>W>!IgI~gv*6orl+Shi;2Ck0rutid6{#`vX)t^wfTh|oQhVunW{M`A=TVIh@h zy<|gJi*0eYXk}r_XRNVX%*Hug##2>Vp5?6gjSdmt62pw#R!bnfho(-^R!U>L1r4)u zS87u>+~Vo3XxM6xC^;rkTpd-~ED^G<6f!kAD=}LhV)JquuS8umnXp=G^7xOv1Z^@) z!TdQ&TstjftX5cYJ*5WHPT|ZwZF_?jxeuc34HaZVY|fgGA$v@e88BJ1u9SYbjW<1X zGl;Oh;HRL_o+5Y*h>zvTdzVaB#uQBm`Sw8sv!M_hp0)=9NT6_-Qq zPi$Cxm5(!um&Bsvw_<=s3tfv|N8&=-Eb@tqU_8ToYj<1ebayT89}|h5Vxr4Mv@8U@ zcJ^N-(n@Zv4e{Qr?EEi*X2r4!irL{YXs;JQ?be2Ae)Og!JUJvK`TWI2QvE`*g!0D_ z`;0$DA>};z%yOE#-#CVF;j}V4#i*+zNf?dAa_3@d>nqp7K_S!CFD3Pd*(S2!|+}z!%AV{I004r6y5YB9c7UHUfvF2*UKCPTH&h09Qs<$Xvi?7 zS~w>gL+YZJ^l_08oeK7r)8{~IrBpb`#iqyDd}%BhH+i?nwj;tH{;AWoWg$JHD43h4 zoOD&#aa3;Q7flq9-uhuP+FN%jK)!2&m!rp{Z-1Pw@X{adDHyZ}CXW^5zRgDuXsB?X zCB!d_XwKTBZz)DUi7)9E7M$2xYalI|K?dVPIx8tl@vsK4b8tFOIjU)N72U5}O!&#S z>)ZLy|6p~m|cN=pVj?98l zimwKP>Z>1 z=w^Q-qDcH3b;q|0=b&$NLFYDmuD}!}nHKojeFMMfp1HnXiP&?*#^cE^ZXkg9*d$sy zlAVbYmQ$UMgk(_<|4XhgQu$%5Ih?*q3ezhHh!SohDR9NawVwESMXh-@+HF9?C}^vv z{k<&VJ+ezq(I2+`>~poLoulke4l>FXDuVm|P*g4~1vJA!qFW*zVOJ0?R6Hg6f^b;p z5!R(@7```sD@3%Jqo9Y^+UJYAZ$`U)$h^2ZVC)U3CmGjLu%r=8##j*@9vG0D3HL}h zpt4W99)7`2p~}?F39y)Elzyz}f|=mlr%B$)3q>G4&=XeGDA8M9h`C5LV-DCEp%1_1 zZrT>YmD6r9vToR21ELyz02k>EDe2KcEpY(7xg+qi$KZ!1U1T2NpV9@4iM?~0;Q5)s z+PD|-qmKHf?$g7LWA&m@(4Mmlr2G=d_u4{vyMCZoq$T)=0xl5*RNK-;{$wXnUJ;j$ zhG|i+^)aUz(LLcateg}RYy1gyJo%#znlw(3ab9`a--YQ6hoB5Ov%RJtcFiaB&5!F* zuiQ5WpWkj=BYqJ#Zp#2dvSHqt(OEAcSPz<~ZU{CNJmTJgSbpGsC#7e%{^_p$o}vVk z-kTo$UKL$ciD>&hU8>S=hm7kC6y?#Pw~QUJ4CRc_OE}QE7|cDBJEF>nIg^ zjiR^7OL-n{fk^udApt2tlY&14KL&|y#4MJt+@&FHX-7*JhKLD#X^#_?pYl6R*p1&6@ zpDW~cBC_LHgeS6IHoW5!Eak5FOvIQQK z0a=7Kxw1XE=BTu_v7(RG$g#x34g0n9@Vnim*;@>Qj>ol}H9Q%f zNcob%k#X}$VHAf;vf}`FFKGuAKmRW1^}fNTFS)V2ay}S{9o+8@$V$87#3mK?t>amN4Q0{=hheE-o6`;ShT z#y>H&VvTxVaq0W$Nsy2eZMr*5H=wj3%H+i4lD+iic3SOSz%8`D6(jW@SPJ6eX#XVD z@)=AN!vLH-i7aOXT#siSp7Xy6fts+_H^0m4xl2k`4?tuK^?ZD4>+%_fYrvzgNec|YH9Cr6)k6pgzf^bk$tkl|aO?B9+HDhdtjcQ& zC~?#*xXt5Inu4{+s7O67`5ek}C6BqgOZ&PZIiCmnM+&UcA*Xl=DRdb1ATtS@COiVd zBt?zMZ4P@|mRJ;D4R?Kf=HEA*vf%=67m!tUj7upw| zVN0B9u5qxbRti1;i8jCx>6hKX-W6xAsgPUCUY_tOfU7v33_gwR6q}af3<`Ps@JO`4 zt?fwt3a3sZS#sRR7HDWRB{NvdjdZ_BB|T<8B_FpU7Qb5&Z%Pn<5GHK13cl5XN%?z! zj={-A09@d}ba6h^kg(XR#8`fm+r{^2>o8IH_4{0Zhs&1U zbDXOHJ|B23@?lITuZW4m+s;xGSghwOC^$LiTsFn`)(yhC`aeD&J(Kvk58ACEmm&}{j2J$!Df)B|_PZ{HzXh`# z2W6tKmfpn2VEkvZCsEEgF273*E}EI)#`UsZ^!RVGEy_h^Wl~s`X^URQnD9$9Mi?q9 zcd+l1n5eVJ@E1*)GAD|0Q5x;D^o*pO5~r$6OYA5Lw`vzFGia_Cw77?F zEH1hoU$X8`T5U^D&+-oH-ssO~Yk0*8suU}QJ#6yEv5|+OyGKNAE9alE>dDWowiB`~ z(ycM|K`B><>ums{)#j*Y)Es1>x{1M~Qzt1AnUY56N-w2<^p?>VBZHeW6` z$lOWP?gtH?Z6#3QXs!T0(9#~F`3Z>uNcXsvCf=jh@OUFncRgQult1RRQ$JeSJH~uT z$DjgLd7k4v?(zs;g^B!Y&J>kp8ZC1;G$w7TTF-j_wP075dGA5L3U>CZ-}XN#?td2S z|8~IrpRV=)MoRxmbD)_V6uk}CHeA3m^Dk0`kn_%Dmxhpu{5GXrc9neP(7Z))S3d`X z6Zfis9+-MN)7H|;ux5AM=sCmlyq(E+|2CWc8$`VZdOoxwRvYA&m_OrOoU2U2vmKpH zIdo|YeqWFX;ua;rj2|mZ2Up3A|HrNTNDf4PKb*cOwSON{W)&M*xx;#z#UUCRFH+|9 zTv4DQ8@sQ_fLROfie1!IqTR$Mm4%}&+Y3N#?O=uT*P?-x1>lTtYGEjF__X<6S7tzb zqs4`mu1P+x+WQw{6{I0;BY{@Bw#;OWYuhYCwA_rd?;xTtF`g}^R}cu_(HeN04yW8H z=|%Jm{AR=Wx)u9V+xYi&2}p6#{vXe{hAkY6)Z-|8$@2=uS&yR{&3QEe z^){sSKRbbz5QhCAc`3i~o=fZQ2^Lc{XHW&JKgJ@YSyYXrPzPJceEChdI~gIcF!V*F zMTA6Iu`s)ZRr}FYS<^5POr=MnLo{k+fbP}-a3jo&#x-#YMbED4#?w6 zoo^#Q!$i&*xXl*%@NBnso?=IFo>6@GHZzOXt0VX5^qdw8lEYW_c`%b@CqY+^Er zWSrp8jY=JM%1v~#9OyWl3G+psc0}vE)Gt-3aUT*YJOG{#{l-(PD^P_}hd5hlC4a*o zw`CX~)VQM`rddf%NGyG!0V_YXTasS2rq+o2m0CQytnJziIzSqfuRKKTFL|@}hG@Xi zNaQKUNpirEJL-{Z1tjGaot8>dv3amjKeYXmD`FjM-!2hw!iv6h4SX5@Z*}xzZ~GAS zt5bmXRZIVl&Gnz1g8vs2X^-mUKj0?>>2tybEf0N3Y(2ZkW3W}ENHB?sKquV+PMII+ zoJb+x5Bi{QRA8tu#L-C@gD57SyevG~pC&IB*+AkNV+v!E1`3F2ywjW}GHEcILYwtP z6(Au|FUB%NwI~%I*hzWN3P%rY1TAm*YEr-ALRZq=1u`{NkN1@vc)Qig_|nY9zV~Mf zKk zluMOyG-0 zj2JIaze?5h#ISEt9BwyYOQN^-5xvIK@J8gZ{a?Tl|Bw|#Cd|@9Uj#qBuTx6k|BtMY zaB#DB^OkThwR3c}cCh>xm65ByqqL}u@o}xW;-KCK3p1f4vCdnti;NOsq7Oj9HVvB! zYgct>%UUq`h5A?R9p3L&1U>vTG(qr13U~Hug7^Z&H0!JX=6n6@FR)_}_?6dz9u&Wf z(3ylSf?k*?3B?9y!RD5)R^Vw$1dqGfZ-D!3wwc~ImbkBI1o<0RlD1B3G0uOm#p4xfWPvtc5s>Hmjlncb+!eK#c3Or{I_yX?>MTg$#kPmNx%N(7t}L&p>qPX zyQdb)5CjW3e%^4yhti!XsiBXx03>htBVFYl3Bt4pf-$)tz_{1{dXiQu4 z7DWP=j+Q8R=3pT(4;!!<&Zk$qu`U*K@RZ17n|1;{-OoJb#_dYT&qs7%bb<*L9#jor zkUyhZB=^pzY?bX~j|@h=X@fE|h!thkw2Duw9U9*hS(nn93gL$j20 z%K?`4s%&I6$D1xt7G5jLNx}nhHR?NatT;k~wM0#6%K&Qf)l2p1KnhA=!}{`ok}-y9 zm`EUjPdQ~8ks!FihL&``o^oY_Hk|R2g&2g`4$NjG{2!=FgE6*^<^TZ7UIg*as7ja% z_b`q+^Dd;MDvSVQVmj=fV#eI!ui$29WF?2?oXIEb(-Snww~^x9uJG+*$!AVV*Z+kE z^pEmL{B$iB`GQ-vf9?MMPlwNcQQcouAP3j~96)OtxT1eqZ4^^17TK*|CFTR58j>kf zUxHO3b!BPLq=o2}A;q`C>^mD2c-IlZx1EZM4=S{L$TqnT#=Hs-wDgj2d(bT2RpNj1 z__{PHq-|tRKjl38Z=d+*pX6K#eSX|h1tF}Eo(|&U%>a_(3LsB58lX@9kYV=|9RK+x z8b%a@w;f9yQtF}KC-KvdCf5Aty(Wa?feVh-%gi$Kw)H4SclDkC;&0nIT++5;K8;_j za!`Xtb^0_G5AHn;HfEZHmIAIKm&aDmIAUYeJY}w9g!?Xa0bUC)pV!b2M zK`XVFWEgGz+S4|YLGGU#jHg()r!!k~j%S%wrn;Fq+6P^u)5&M@tn^f#MIF_;(=Voz z2>qFN?o~u5-1p0^-6|awCkBH2Ryy?=?~WZfZkh_&BwOlE(+&1LR%VK4Z92Q5I;@wE zF5|}vR^c1Uk{*{Ki#!_yjoLJPN22SxoV0B^jD(Y&aoRqW((9Pb>KK{9dXnMBJD+80 z=#cZQlM8+Z%S#VE1ufd_IRG~R_;{niQlyGa4+p?Fzs6qBwm3}3nxl8u-r}I2=PcV0 z?#WqkYFp9B4LkJ6wC1E^1cq|hB`2+?%w#R)VYP6&j%&ZTy*xqJtu_VEq*wynO?)#i zy+qeHGBw{Q3I52Y)F_9ANB?^YHu8NcHQE^4Wqr9N!zA_jid(6+LGhxXZ^z(rG|~? z;KCkbY`1Ee#TZmRtaxHyUW83RBzRJXDkOxy)DI?8=km2`Lrx_Vu%SI)oi;f4`EZ~n z_mmOrK-lcI1?*|q*X6ztNUm$Hq^~7f8(x+XrEC`5wreAPntv`wW_N%%JCN8eNYI7H zR)0@c7-HfH3pl`#;w(G~c)ehQpoZp7-2jsl`d@^7Q*>o*wstD1*mlLXZQEAGww;P? zvtm|kCp*TDZL@-k{pagG=Rf`R=yA@)8e?7ToBgi2=9+jQbdIb|?+)3F^h%SmDQJB< z4Ix~9Jv==`KzPg32p{Z^VF7u}1s9X6CCnMd2rSI%PVr_^K*x+9BF-(`NREnA;zbPS z0LDeM1PxS)bvoL8>7%8}$U*A@*-_1`qibBK6d!m-ukckkXFEdP1~W{Jq11j4s?AdB+iXBLr}}1x6t+A(_feERCvv%F)O# zn!H1yMndYg2<|r@llTbtse=*`p8)j>FQ2LHRLV|mRN^X9!KsSr32bX$!*QO1Ydzg( ziNjNv$}It+FKo8bXFlIYmB6_L>n!Z{6$<|mi|=uu(5ME-y++v)v}P10P4Wih?OqaH zB`7OADRB*^mD^7~q#LOhnnUc%10}w9QEaE^B&9~N((bpy$6kM_^`1Y=q}AJdF#Odv zC>rdZ_`@dof#;@@D<<;EysYs!S0@!BU;X6{E@!UWFHdYFQ@){qpFkw>{Q-Wqm$V*f z+6=qS@q!jy}d3p)Sip0;2MbEC;b23Qpx9>@IrmUA6-6YAWZ*fvGR|W z>fdk+{{wvlxFKhLYGxe6tcFgbR4r71H;P#mLc!3%=;J4f)VHq0yv*dYcr6+H1ahj? zcmd=XPAM5zGn`6U-m&}xpK}}5zN)ld5g*-rW@1l)`~9gNe{ayA!D?V{9x}rVv?cHs z=_f-}6JtBJL80#Zrpo-4-71!A6Da*fje48Uye;C1zLQ(-dT( z5k$I%Shjm9_!9H2&XYA_@QKsA=2#JLbQdqL)eGT)(@tH}t4OK1T)?7WZDytbOxbLq z&4F=Fb0efuIoa*N*f?3AXj6O|a=RV&pkmI#X*cPpO=;EgDV~BLCIai?ra}C?KVBm^w1mIgO&|KD|S8sw1r}`U3VxO7p=oE?Qv-syOkd>KM&YO8LtJr!t!ex z;F609qgL-dyB%C=5b=IXPAZ}rzPi4(T>ZMNthtGf9l%6(4|JNm^I;kWbA-F()-Te? zCvb^Dn^fiXvYM^8vMS-8t(tp73bn+`-$*mbD0tF!B(T{G~ zn=MG-Xi_WK*J*+*`LhaU(ik>q5bv2-4JyhyHJO1Kz$)rAUneXj(l-Cn_DKd3Q z_>0fR5{HaT5FUG8Y6HXTnoGVvzZDm)Mn+byxDxB8(M4{D*EC$8oSSM|nxv)yi=qQf zOSyvrM#Hj3>RaOC9S_6-soEjK8%&TPB88+%CTsC(X1i*q0oCS@<&Hs~$f(G)P332i zaIWeyzG;!(6@u6w9i`h7%Zxva|G*MgBBumu9;biJ0De$bT2&u--CwT@DVZrQGONFr zf%6G^f_0Z%ql=B|A{eh97izcKiGT50JUU1I+1 zF28>EK>tZt^uIgIf9|10T%G>EI%$4pCIVPXt6n{-U%yBi{@0XFz+A?zv7?!!6f4eiBsJDR2MS#$m;L}u9(fuc+)Pk zGVYc>_&;EKhm26LKT#tx+de4BvJ~GDNn&7W>FZku>>^vUMNu>~b>(&?UI$j$FqY_E zZ4AW;SN^=Kz87G&o`Dt!$Nt>dbQE3FmmuUnYmLpt#G+QER(Sw7GPT!(#w+8lZpY`X zr243?Y8pxU<>k84&fpvNdm4LAF04XUov+7myt#@aWn;eGRzM{u)`rqs?HT_d4XkJH z>8oW?zDpCNS|g*{iPzj=arm3|D8TC4jFiU0+Mu{9wYE zXc)WKDtD_ZF=>A2uP{FH$S|B%{KB6?=QJESv_79&Dn9e#Hc;`@U@LOVrKx74VC3Vh z*r6f5JaScO^K59hlZ4U2lifjzQo=(^fBa^)e&zym(`vYrh6eopBZg~{9P-njTkyv% z{aMPYirSJHa%1XD|K?7E06jFvC5<{|L(U0)mOA|A1f{K;J-hr&o%wNLIEo&ztad8b z9IoN&tu~VADb#48Nms!LWfx|Y4pQ3^){6MP5PN&ba@zWG)-;|`L;6d&hvPX3?IOyh z+$e#hyixi-`hWvE^mAE#tN1|;YBKh-Ee6@d4yfF3EU|-*$#?+PcUB0h=pjauB7)%v zq-;5c4|dhpfXB6*P{%!8MsSslfZCtEqd!K~{r}AN?!Wf#i~2j6e*Z(2jgX`l8GH5J zDf-3!e2>V^@DX-xF-xz*K7P22-ytg0BdfVpy6FO`h^@V;qhxQ#ZXst!E;ELC+EF&R z%X|Xr2OQXun}1*ATaadB-ari>DKii0aTP$g#!_Y7&J%YO^5hodUQOmCEK4#aeV!uP z#>k(FkANHH4dEK%V=Lm2^{*no}15Gb9+cd8z-WB%8DV#w{ z!vzCe#B*$(<{k0xZ(bz}_ff%Td-eLs6#hS@8vlkz{I5}|x{fodDVE=-Svt+SbSW2~hy2-E$pyA@z_C)?7Zf}K}8k(_X@vfWl*e6xOQ&X%N~YIlehTpevT zVWK;36`{q)Z>_cR$jL!5c$b~ZjD5tH&8)nL3|`v4RHA3C#bD@>$QZ^vCpycT$$WeL zx}I#8yHqmEekCg`qhMtwor_<%mMMp9Wh1xC#g{gh=;fn0UW`)eYOBqmP;|$&Q48fg zrIL@%YNgyVWV@-|0cc9UnK7)F)CWsDqJCW4#MjU3OC9@}FS}Wb*4hpLz<{RkB-(Oz z+OaE)r7zl3DPj>+MH6)D@rv2UB-P}p%k9glU|Os#-l-`EGXa8RRlYs@=*uKUHjmYt zmsX!aS2Fml*yRmXZARN_)5#CVpiNn|trSqN*B1?rq1CatPz$h-yq@a^(b^Z!Ugv8~ z;xWqozW@1~0*`DK+gMISX5p2>j_kH7Wekf8oj1y#IEe%Gc{F+Cm+!Cad#P9uw{323 zRUk*U{FxLT-*@txT{Y>k_I^zZ`3`($W_1f`MC>OQZIpGW?=;QeNQpaIHX74+Jti^} zKoI#|RS=~CRmO3=XdJ^z5Snn_H&f_s#%DDX6W|ylK`)5p1F+Ok2k8*B)*-0ZiVsLmv^Ba$TidF%ic%gx zip2O-L|gtN&Vt(2dh#ZA_6!uWt-?LBuw=CW(cM6+LkU3Ioz)&@ZFA+;@x;BI->J=B zIj&ae2_UB`B=-Y5oermOMMC>=t%8OYv%+FJ#04VOIN*qx2XpJ<>_=Y@O7@J4^X=z_ z@^lmE14F_w3U9(PN~br0v53hf$~-u*I&Ww1k2l1EA;<7W*^x)ICB!2&YK#HTHu4qtw(P9h~ z83gYTE<4t!*rrjt;zCDj6O`ZNG+((9^$R%aP*$$7?X;kPL&MJwhPhvayH|%a_=Vj$ zg^;)%B=0~Hv|##3U zQ!C){u81lK`*!Ju=hxM3|Y80L2%Z{s7k7qvQo;4+Tf$1>0IOW>EaQFxZ z@ve;y1wG|iH-;_i*;B@Ne$deGTTOmZO zeMNZ3bU|WZA|F8@AeL0>pFz1Qj77;@3F3c_KzZzUj=dtw8}tvBxN5~9nX%t?WDxfk zGyVcu7&pyIjuLU*5`75jCOP%A-xZe%y1MV+qkKuC8E~o}u-0@;-D1pw4ZgYmh=Fs( z{-Zkxp>sUSv6OTbf&B(5A7+h&Y?>t4cf!p3=lpHQEk;?_BwZK}_=2xZBc{Gq>vIcd za!aJ##w^JqkE_XJ4UoC$myHS2g173(iv@tM|1n@;=r5!Qo^3{x7l1go2mQzd*_js~cXLO&{ls;!dD_t# z2t19ta*gTm$rRwA1H9(cy-B?Piz>ulC^W5l&l=}X?po+43hn>Q_p0{Rrgnet-(oa= zYv7rqfADpHHFUuJpog~9B82Vx96c_uGNgh)=c9!~B^Q$x#Z1=njLWy5SFtn%H!dzc zCN?*(vQv{bv8l~S+BOQyUWe!Il}p48$GvU)D+(OX6n@?tcTSx%(!w(NZC|k5tRJ~w zzHOwLKmPXLeBuhg+WccEpiErCC;74+csdy70F>C3>K=1oA|XEeaTtV2y)_!~*b3Q> zn4(DYy?3A3;D=WB(W$fU5h$jD<25U0-qjAakMcmNPb(r7BLAen0ASF(?GbOQdt~sa z?H;rBkr>ke#c?#qoX-OohEev7_p&dI&!aC4^BSGLdu&if_s$eie+Mc}%tvluNz5lG z-|D-M&uxUqmc^9j)Z>U|cAtQYd~S*{HWv?mR91Ut4u8MmN~I~h3NRDZiM)oU8d z^;Rf}h_mQqj){wcVpf{gHnVLxpsVz_zWn6Xg}CNSdXd(KQS$z#^}Z<4^a>}kiek1f zv#%EddyHqH%F2!99DW0yhqsUL)=J8$+)8qi8t{}sB}R}|$_x`HDl?J+t)=nQ@TSZ- zN4;XX7RXzO70UZ-Aa*=7vzC`8^1Fmrk{$#T9WG!DZ}1W!L_q~k%d2*81sleG*s!e{ z>j^IB>TZ%OIlCOCcwb>rc*V!eKkCZ)%YsOfa(MLs= zK55SU8_!n1-_S?1A4;4IPpC8)0k3GRg?3+#4Hb8B_er#!p)h$H5+?VP>t{|$H3}V!;C`d4!e}`05vBY(Usky6*0D4iy3RrM+0ABPA?`_ zP3rr)5RgsyJfbQ+mm)mfht!5rB#h`DZ-d8x*zt!D-_u#Y{^! zT&%7$Q=`cO&OB)#EsnD6D%K2va;e%Jj$Y6;ayzD+-uQHgAgV$Ju2g=6-Rj$S4-iuq zdxBmeZGr*@XiQ(3^L^*wMNoUGvlhU6ak+30k7Nt%25H=E5)4NUXeKd7UqO%f0kv+D z--8^Kb;2?87%}n^%vc)H`$(XGcs;`B%na&^l$R4+;M=gB)f*w3FXHcc3=l`Wu7D=5 zaJ&L3U}>oBeeE5bs<G17_i$+u_iib1^0Yt@7^y5?WnyH?Y?_rV8AQT$!n z#}g8aCf-O50LrK;!FHwH_hRCQahs7zL4v_rc$Wa-9j|URRrzQ4bEsB%10nAIvMOPk(0J=x)RHeNO-CzzB%|4(!y}Rpz zK?p;C>JHKGXY_eD9oEHpj;ik*UAD`TvqbfO)93DBZwgrV)mb z3fG&wWkx?l60m2OND}jd7P~F7b}9q0^?`-p4ra5cHHi=5{oyS3i@4k(c+e8^=IDqSFl9r(^aQqhnk`hIjex-l64S9G@`D>)@dl%F}N)sBDVH@{5xUh-I2* z!{Ift)cP#Jq0RV3%=y(NyJcy*vOJXjk}&c`-O1-c)vv;MH}GsjS|)78RYQ|S{kj;< zXG4&wNz+do{6-rsk0C@5wfogtBzq%HYB81y13-2UVpj|B$l^PWUxA(@Poi|B)ODlu z87yIpKC-OIS{Z+u6dcRF`e>SXEp9H%9Z2~Q1 z|Xg>l4Ve?hB;{$(k zoUpCbufTU?DWkpO2#6E-T#F51;^$lsLS`Uyk3>HYMf(T(M|)jdxS=*{!7xxf>}78~ zKwr=TlsGVX8yz(C8Co6aE_^n@Btc{&61=E-qlfs2Zqc&kDWoWG!|jdz zVN1RP`r_v=lhfHja2?DFKLtE;iQMM1QQ@p%Gl|I077A+3Jd{e4O!{aY-*{VC+a3^& z<;6&ty@^-i&~1lz$t1kEe&-pQUZzdbJBBSItycMRjG@&bg@qqF&sBD0&N5jH zGxcQiky>YP7q$Kn4YhcY2l|WZIi%ytCX|=(l32!>v3i?6dZ@Ea9jR5U8IG#!H$Uxr zjrKA7BKSEeOtICnv-Vr3U8eb12OS#Ed*!>IFp!@^3Q*{$Me}qbMB5fJY_ThbOt(;xM@4ogF==VsW)-2MV{RbK zUf``tLLQIP|1c%^K6#F^dO@5zxgqv~->w-{W&ib@q>aFuDH08VP=ex&<`Sal5(* z^{^f5UhkX?U%Urjt2sCp!mPiEFW3#wtxi>8eAuY|(KmQWQss20p@%571M(mZqzJ&~ z)fFMjONcrQ>ydDgVUrJ41~d>+M6DE5A`urH1Slp|3dV>Ell3dkRSM21UgdZ4O;hDx z2vu9=KHeJ>QCmIs(I>5>KygD-q8#?)-|nz_ZL&Y(kMM~;fVoVOd#Bdk zD?q=4bTzc)lkwh!qt?tulFA(a=v9A?Bg-k~OYdC?An4G=FG#7X10*Ps0+c3H&@?n6 zcCS>#6W}Ba6RVWaFvJW69MM?yNeVPEV|Tg5>adUpl}=%>2kS&@_WC`3%&Wh zkMWq^%b(BdFAWqZ6!n#_?)eFPOKjh#NmET_oz*;2wk+&955g*SOA(a-Eg%t+$sQT3Q9*{yKZg+OZ+RjpIzhR95MYIYGmx+S^y6}#~$LiQJJ+L7SP|sxz@Lm^w z!hk7emMXVnTt{N$MU}nA_Kz$Le8zJ)Ygj<~$lXT_{va{^Q%b&D9^f%XunVVB{Tuh= zU!2VT0%i=nMJwujt^&iL|Fd1-f6wgy=@4gX^3UPne@_kdoYzFr`0DY;9g`PbXQyYU zxyiozvwk{RS@LV6aOBeuNo5TT%H?aV;pzyb^48B=rtd`f_n`^|Kz<<6O`#g zR!F&59vlEX)fHxSAJMOE9J;mgT4Pr0Y9}SqtVcv>r81;9PhViQu62qjwN71-TuZGe zW??g{&Lrp2c3wm#z74cw-(t>0DWc`lRCiryry)Wn0E!d{Ml*|tVC77eb5E_^p>Vis zN;z}ln3kZYmq$LtGJRiAjaOIJEU5oGh=Vd za9pHo6N1qbdb|EA#Vy_31Z*CNIIP7r#Q0J;-t#8=Q!5ER_Y~M&%KbR9kGB{5yb=!M+WX5udXOA2BF~)paWx+uH=Be zgr+;(fnpZ6f+W;X@e@LAC_m6%Xy9xboi^}mE^hUEc#(!P^it zzLtA)zQs;M+qIZbLzF+!`l_5RRrEtIGTO^8P6Vn#E}W`e{RVd5rX!-v#fTuZ3j6LN zgK>lK9x~cfuq;=LON>{U7QAwj!pc3^d9HfJI@w_=|LRW5qQS!-<5pu}8rDgO%MvVi zn!NimtyUK1`80s{%1#eU9O~}QJShAgZ+y&1GNi+}^1f~iV7?yd5Hpc2Osk=-HCiGydSSJ9Ob)s={j4GknO1CtVO=g{O)@3DF8 z^oPp*_OL2FJ9LI*S`2qSc_g3#u~2Zj!hi^-gXfch@M(d7xGSbX?yL`rA}{~DOY7y0&d~?*sj@kX}^-Ouf{z z@7(`ANzLrkrh|MYsiIH3!v8t$k~g$9{Vxer%l~p(i&2$vMwLK%AM&bFT-+Y138*8% z8=j%D0VQIP37``}N-z~{4*x+Ky)UG_WTm0?t1Mn1K?R|DEW_c7s2nC8M$I$3O!@%w zAX$2? z<+5$untq55KC;(qk@EXd80-i0SKCcAxVl{g{}K48HP^!Pwh}!9BPhOPAly!sW!9!x zs5?rKT~Mk?$n0T`Z6vP}tF6*0Yi0k&w%Vs4nbp@gRLNEM7@-=&W^87$;a|9&gVykd zth_a*tYRILXZ@NxwhijUMKJI1cI{skDq(AirWHsN0ap?u`MU;0yIkEFMBkyaB&nvm zDsmtZgU4$*6j`-bF;l{^K9bYz!hSq^cpc&6NA z)=M$HtR5XZsA!U;(2|Nc;g^}85cQ>#5V0B`9aOrw)zw9ZsC(1Wy~P1F6=hC7$r-H- zkzu?sxEshPm{hXUb#1q^oRZouedP)oz@FSYr5%rcC-tt|aJ@+&JU$EOFJp{SHk;G3 zZC*fsr2~1P{n;ZZuBfNH1ZWgo!&`#Hca|c3UpSY}s0N(oeJ_b&uJr}SF9fqZ#}x5~ zuf54>qAv{``Bf#x$qN9co|{QnUm)2qjk`A8@ZG>Y(O8*(O1CBFEzHpKFIH?;U)Q)y zd|9+2ne-Oz{!O!VTxs(JBMvFc2fpqLML2@~a#6z(p)cmgAIz~J;!HoJv=t0t=DvFb zy-SgBMx>uJpD?XhXtQx=0kTN<8l5SL13K zur#5B%s(M2e!3)n%atdc#bcvDTb3y%AVFJ~`3RZOE1LD+eas9_Rl1TtH&PZZ)O+L_ zayy|HAi`RPJ-=hSq965UWiptZ@DXGA!`9Te3g({38WbQY*``n9?16Ym1Dx9N!Cbff zlb}CmTw`ud8hdn=Qmc$GHe_KGz)fb4#6b>!8cWS_5au5B6$%I$iB7)7lY?)2%Ww1+ z(;UY7O!6FP@|51Kko-~l@7V;5|6B~_6Qo~Ju}_>O$e)(Hdz{K3EPO5wbHf~AQ=Mp@gxmoP3w@#|`g!#Ay; z57Mp6&oSo=!Cg@H>%l*;t}iiG?tBT-zu`Nvg%YUcOWWzMKT4C+-%}S$N2jtrta#q~ zlTE(QFTx$KyBO5jLWZ@BV)+;dncJa#?I~C?@dsh+paif{ua(^WJ2fIXyGc)n-z0tX zZEjGxw09Xoq+rMK`-Z7|KH{{f4*p zmr;78auHV3#+KUcs&n?xTo0&PDIL%a`l{;`6NX6^R8k_XgH=yY4Rve?vo7vdfA}hcyjm$jh2_#Ng?MZ>g1D#T>+Pz4^IeAJzY?8--!*$$p;$Aqj`{Aoo z7q#w+a*@zuj!3gsvQ5gRsh)zF4Ds0_zPWNapV$%*HEnZQS1t%-@G5cw>UBI-_i z@}_v%suJ8aZ|Ks4-j6n>2jbT{CU4JJF&cl&a=($Gov&xi&gvHA4N_b?Un{=;s`Ki} zYvSpw+ou`Ii*xtX!{OQ5n&^dK6TI)y`H-|?`8#A7?CtUGoC<4zkPAs6w}OJ2IGezF z`9~u~pM{(Jj8~>z4Rpp&-!V=Q+>h99hU3)<7Xrd-+kLs4#X`HGNB+d25q?%2)YK%_B?$9^av=*|h=j+TeAl3NG` zx;cl|RFXJ}bdp=hDHFf2_nK1!aU^N;eQ?I{MAQT zuh|D&e?kd|KRFQ0{|lt>U)Ux8ZWkHPClC0`T=*-hNB~kqByK?PGFTC)s3@{fktCWT zyxA-2uiqA(;=5L8fB8v$IOma!#nvE&p@#3H&Soyx5y-^v4zC$pJd{7QCP!>H3wk6{|+B z?XiL|jQhvhIA@x4bzfq2nww-f=e9aDBWrecYp}X3wX8`CCt!AJ*;a3(ad_>IT!s{6 zIiIxCWz-yr4@;xnJ0-El#fzJKoK%J#!<_0qu|HHlZ z<=-Y^zrLY_KYSis>E{9dCtu?K&PxBmXfr3iVnuNzsQJ{c0;YTHq{JPFTAcn?K?#e%8JRrjO?|t!*(ZCo`c>&iC*&r)ipT7i)UYCf@5Str4N-o0{f-u{lNUnz)KnY9 zqajzk^ShYEJ$*{%wJzrs^{A3zFt38o4++*gx@@`$VW7 zO4WMFlQQ(Wi7X5qSCOb`2lI{;-IoKEpuouu9qxzw3uKQfj$ABgt37QGRm51FArA*x z1@b0jzE&w2@qQgwTShs7e(H&BmB@|i#f;A8 zAqml7i2@qb(C6_;OOt5&Bjv^kPlu2VF$Vi(R92j8s7X|Fw~iL_t`SwP6ZbGkx?e9t z$TKZOC}ukYi}RVb_O)*R>)%-A)C$TIYjIRr|yl7`Q{Z=|{I)Fve^=4mf&Jj8yA3fec* zkTVp@7*Y{#XDOE5eaUM#wmR(&;?b_UB6eB6>X;pi`+92VL6wNwZZcx~d~|Sz5m)8H z$lu%`jD3P-sqoGwSMH*LH%dvR3j3djJA@$IP&3cD{S6 zIK`&owTQ0LHOY!Dz!e<_LhTjH{?7>ewOXLtO$HxkOJR~-=R%ir2Ccg>b!&G z_6(%oZY`xeN|WCWWy<6}4B zAP5#74B&Af03s?j@)7zpwHT_zhyKZR)oi0GQu08e51tg^%ZRZ6i1(g16#`WEhfqBd zc|xk7wZ#1Q^dWn^%gW9X&fnBxbsYSTVT*RG)LtKyJRt|)><%!bz3dB?FV$x$o0?WP z?t3?IHBXuij!aTA#WSV|9M*JB=P0)X3^6$bKEnKGff(&Dc%{{pw%T#^9H2FaPkE5ZJc3&cB=hce(R=y2)Z93xA`sw_k0wyVGH+_m19s zCSMK5jsY~Md)IXluF=+Giy84fw70r#8hI!xtBD0Xj=7|DlU$#Dfs2wp_ z$rW#!GBBn%vZJR%ws395U)pJE;@*&N*@IAjtV}f%0eIq$M+(klzM;w+y^JPbEr(J! zU(U2Xob1Nrjvy zl^Iv|#4vxap}K2KTYT}ULTh~mP4_|}GgTU0&N`x9B zbJB4;KQz}_QVNrRsN@73sT#-4$WwZBZVkVfA6bv`1t&@3aPH$)&5N2qhpiEjDP7FZ z1a*~c{TXhQ9L3x!#$_c6jx$L7Rg_H|{H#PXP`(bX3-2Lf6|ziZc23we{4xr%Mx-g^ zG~VMYSwF1WBbL42d?d){Q^oA_Jd0;_^1{a<9CxnoVkTHKLRC5^Udt-2^Mz7CH_2jX zs49qSunD7W=HqI%KB^L_baE}r7sSsLb7PoXm#=qEI_FQsCtmTEy8n{65r@DOK<|je zw~OQ!MS`)yI@wQaqJ+o1LBs6L70FYTxdU8;6YJ(~h}HiYP1-S2fmT^1J;T_bJYkf9 zSrXn)WK@RcNQ=H_!u^A-zgF{bG&Lut#Fe=tO>;#rVbV^FKcyipAj$Z|u;@LLR+3gW z*!KyZcCU36GhVYs-dJupm>5?1O>Ng;MKHWHoZYNQ`!|0JUrbZU2vOwYLG9}LkZ*)jbcTL^CpC0gqyA>z* z>cV>ysT?66cHQ2gP~NaInr5-pT0cc;{}7X-iJ38o7-s;7MBGdeuw;feN}yQI$QHuSN&C^{-^r%Z>ZwGqs@Hd_+a`4k;HowTxFzLyWX?87(Wz|_teAL11;*B}e3+Y2Pt>Tw&PjQ4lno-(!S7RU zhJ&?}lbk!SsG_l@MN1u`!Vi*{LBRX?)7eU7sNBj(*n_991Zur^ILRcz z&sl}np#Sr~|Jqb@QywbfebVMRKkM@UL~8hJGp%CkYV2ZZZ}-pjh;72S zG(Zq7^keQ&dGA-Vnqxk3t0V^ytTkB@JbyayCH-4cGGjzre$8P&Ga1Us=AhBMdJ z>yZG9IY0l_<`*cuHLMk^wR9wME=atk&h4YfD5s(KNay}Go-TzO#m5W+QQCs~i2+$T z+!eOIYkg4Q7-vdIj0mm-K7&H4qs(Ji$?qO zT4TMPkRPp{#sg8sGln>Q#R_!{suu z-EqYUf~80lpi73ovwYK=#fub{zxPmiYu1Nyl;CmsjE27j&R4LKNLqBQ$Tcj=X7Ex# zbKRU_2TpU?8$8QAx1w>`Js+OWzx6)rW5rvgpK-PGd69lz_D<#uh7N|t7N!gqpU2Da z&)E8VP}#=*Bd9{QNM{zx2v@ZJkVg-xbm@r)et%OMR_=pCKiPZ)tGw4%-TA4 zOk2V#=`fPf{u)R*Su$T4eJQK??teks-*{~T6r+txlNDDVrY2$@#-HwHl$@fz_Oq3=}5r3XYZipGw|-;saFL#u5a&%@Y;5$BH2 z?rywP`upDrQxZ`BvWc5sMW`N(xiZQpvHy-*Txe%5N%?F>3*97W>B*iKYvM#C&O{T_ z8w`gMz4xv|yBZ-4-SI<;SYbkxGhuUJHDlO^PGp?psZQ$P@sT~bq3SnEcDbwy@kV$q>CdF=-XkS4D zq;&}h!?n+8&nM2$ocMCSK(X4++sxatLczOACfh^QvF8KP`kcDR^0TQ~_7k~K@Z3=@ zL5WaLYEm?Dlfsz*Nf+L*>X7!X!*}na&?%nTzkS$XNOBK5%k%FGIpJLyRn}ySOIi}V zrHYYy%j?^bBpX@)pcZyE-F80RuHtMEM&i_BH#i*}+az)TSeC1Y)WTfY&Q>Aq@ z$LQpwi9v~%V89!T1)GYey*5bKOvc?fY}iwXBbf_Xp0}gz1Gxc7Knv69AB%M?`a{?` z*9_CxzDmO%y&A*L>}zt8fCHkfhn$9^KRo9CM{BQtwm7yxC3h==zeR&PkcT^dPN8rI z+6c*Uhzt|cjyyd1129Qf4U(@=^zVf_eS;LFk;*}4QTnl9i2)sBK|EDq z``B^*#Lo1U{BG+~Jih(?vNgcJccKh*5|EjnOARbiwU(7Sp{8J(`>H?`Pud~flQRbc ziW??JlcoAcn!A+Txj9E6wsm{m32LgM$|}wyUZ=3DNf2`M*aW(TVU_2YT6(*fYR(A& zZ4-gA9yWqAK^3A~UIcL`tTY2{t7CdysxoJ_y1B(8g*`ccyD{lP!0k%iEGS6>m(Joj zNUhex^(^=mk0aa7XP#*;=z}r$b(6Wmjz%=Fs+hXj3g4LqYJwgz(WOaIQytQ_VP1-w zT`fCPx!hVugOqwTGZ$}!YSD09`7oB#Z6dAF4#(;)>zbPozaw4!hMkOTwp;j5nO5WM zsbdW8j5aub-xpz4M@(?9u|5b_y-Dh4LmHiICHS={%34FN02@k{3SJf3<$A+aC+U=! zf@%uR#q3`BsR^Q%^X4Bm>1FO6O5{3OYN((*WBLHVnh&m(3J3GaL6lS5%p?9D|8{^Wx;KA=;#iD*82u0UjIK#7zO{4kJ*@wIZw}U2+ z`m0h)nAbryGXu}{g88?yrTk(t9b~%6=la)DA>-bHDUX9rXHr!$=;Ft9MTTn)>|$7D zlc+CQo<>ZxpuwlDH?=*;vy~p~1N(!7%!8>*Kuao2DsFS;fyS~p1`YCelE>2;0J%*l z!C5W14pD7Px1wu!K}xyAk-Ax414VXMgiW|{;|1Xfxz9c