From d23810d57aa7f0fa67070781645f4898e20db0ed Mon Sep 17 00:00:00 2001 From: Mathijs Moerman Date: Thu, 6 Apr 2017 19:58:12 +0200 Subject: [PATCH] Expanding controller.py with basic user functions, get_models and destroy (#89) * Added user functions to controller.py, as well as the get_models call * Fixes in controller.py * Added controller.py integration tests * Small fix in integration test * Removed typo * Disabled=False in controller.get_user does not work * Added grant calls for model and controller, add-ssh, remove-ssh, get-ssh and get-machines * Added last calls * Added model.revoke('username') * Edited docstring * Added integrationtest for controller.py * Fixed indentation error * Added async * Fixed type * Fixed typo * Expanded model integration tests * Fixed typo * ssh-key * Typo * Fix in remove_ssh_key * Disconnecting models * Added missing awaits * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Removed await in model grant * Corrected remarks * Typo * Fixed tab error * Typo * Removed typo's * Removed list from await expression * Removed not needed await * Added missing import * Test * Added decorators * Merged #102 * Typo * Updated _client.py * Merged another --- .gitignore | 1 + juju/client/_client.py | 156 +++++++++++++-------------- juju/client/client.py | 1 - juju/client/facade.py | 6 +- juju/controller.py | 82 +++++++++++--- juju/model.py | 67 ++++++++---- tests/base.py | 13 +++ tests/charm/metadata.yaml | 5 + tests/integration/test_controller.py | 69 ++++++++++++ tests/integration/test_model.py | 37 ++++++- 10 files changed, 318 insertions(+), 119 deletions(-) create mode 100644 tests/charm/metadata.yaml create mode 100644 tests/integration/test_controller.py diff --git a/.gitignore b/.gitignore index da5be27..866a785 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ __pycache__/ .cache/ .\#* dist/ +dev/ diff --git a/juju/client/_client.py b/juju/client/_client.py index 0847da6..9746815 100644 --- a/juju/client/_client.py +++ b/juju/client/_client.py @@ -7059,7 +7059,7 @@ class ActionFacade(Type): 'Result': {'$ref': '#/definitions/ActionResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ActionResults) async def Actions(self, entities): @@ -7410,7 +7410,7 @@ class AgentFacade(Type): 'WatchForModelConfigChanges': {'properties': {'Result': {'$ref': '#/definitions/NotifyWatchResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def ClearReboot(self, entities): @@ -7580,7 +7580,7 @@ class AgentToolsFacade(Type): name = 'AgentTools' version = 1 schema = {'properties': {'UpdateToolsAvailable': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(None) async def UpdateToolsAvailable(self): @@ -7614,7 +7614,7 @@ class AllModelWatcherFacade(Type): 'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(AllWatcherNextResults) async def Next(self): @@ -7663,7 +7663,7 @@ class AllWatcherFacade(Type): 'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(AllWatcherNextResults) async def Next(self): @@ -7755,7 +7755,7 @@ class AnnotationsFacade(Type): 'Result': {'$ref': '#/definitions/ErrorResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(AnnotationsGetResults) async def Get(self, entities): @@ -8078,7 +8078,7 @@ class ApplicationFacade(Type): 'Update': {'properties': {'Params': {'$ref': '#/definitions/ApplicationUpdate'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(AddRelationResults) async def AddRelation(self, endpoints): @@ -8433,7 +8433,7 @@ class ApplicationScalerFacade(Type): 'Watch': {'properties': {'Result': {'$ref': '#/definitions/StringsWatchResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def Rescale(self, entities): @@ -8553,7 +8553,7 @@ class BackupsFacade(Type): 'Restore': {'properties': {'Params': {'$ref': '#/definitions/RestoreArgs'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(BackupsMetadataResult) async def Create(self, notes): @@ -8706,7 +8706,7 @@ class BlockFacade(Type): 'Result': {'$ref': '#/definitions/ErrorResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(BlockResults) async def List(self): @@ -8786,7 +8786,7 @@ class BundleFacade(Type): 'Result': {'$ref': '#/definitions/BundleChangesResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(BundleChangesResults) async def GetChanges(self, yaml): @@ -8822,7 +8822,7 @@ class CharmRevisionUpdaterFacade(Type): 'properties': {'UpdateLatestRevisions': {'properties': {'Result': {'$ref': '#/definitions/ErrorResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResult) async def UpdateLatestRevisions(self): @@ -8995,7 +8995,7 @@ class CharmsFacade(Type): 'Result': {'$ref': '#/definitions/CharmsListResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(CharmInfo) async def CharmInfo(self, url): @@ -9064,7 +9064,7 @@ class CleanerFacade(Type): 'WatchCleanups': {'properties': {'Result': {'$ref': '#/definitions/NotifyWatchResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(None) async def Cleanup(self): @@ -9784,7 +9784,7 @@ class ClientFacade(Type): 'WatchAll': {'properties': {'Result': {'$ref': '#/definitions/AllWatcherId'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(APIHostPortsResult) async def APIHostPorts(self): @@ -10415,7 +10415,7 @@ class CloudFacade(Type): 'Result': {'$ref': '#/definitions/StringsResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(CloudResults) async def Cloud(self, entities): @@ -10809,7 +10809,7 @@ class ControllerFacade(Type): 'WatchAllModels': {'properties': {'Result': {'$ref': '#/definitions/AllWatcherId'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(UserModelList) async def AllModels(self): @@ -11179,7 +11179,7 @@ class DeployerFacade(Type): 'Result': {'$ref': '#/definitions/StringsWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(StringsResult) async def APIAddresses(self): @@ -11484,7 +11484,7 @@ class DiscoverSpacesFacade(Type): 'ModelConfig': {'properties': {'Result': {'$ref': '#/definitions/ModelConfigResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def AddSubnets(self, subnets): @@ -11622,7 +11622,7 @@ class DiskManagerFacade(Type): 'Result': {'$ref': '#/definitions/ErrorResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def SetMachineBlockDevices(self, machine_block_devices): @@ -11663,7 +11663,7 @@ class EntityWatcherFacade(Type): 'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(EntitiesWatchResult) async def Next(self): @@ -11726,7 +11726,7 @@ class FilesystemAttachmentsWatcherFacade(Type): 'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(MachineStorageIdsWatchResult) async def Next(self): @@ -11950,7 +11950,7 @@ class FirewallerFacade(Type): 'Result': {'$ref': '#/definitions/StringsWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(CloudSpecResults) async def CloudSpec(self, entities): @@ -12303,7 +12303,7 @@ class HighAvailabilityFacade(Type): 'Result': {'$ref': '#/definitions/MongoUpgradeResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ControllersChangeResults) async def EnableHA(self, specs): @@ -12386,7 +12386,7 @@ class HostKeyReporterFacade(Type): 'Result': {'$ref': '#/definitions/ErrorResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def ReportKeys(self, entity_keys): @@ -12460,7 +12460,7 @@ class ImageManagerFacade(Type): 'Result': {'$ref': '#/definitions/ListImageResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def DeleteImages(self, images): @@ -12572,7 +12572,7 @@ class ImageMetadataFacade(Type): 'type': 'object'}, 'UpdateFromPublishedImages': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def Delete(self, image_ids): @@ -12822,7 +12822,7 @@ class InstancePollerFacade(Type): 'WatchModelMachines': {'properties': {'Result': {'$ref': '#/definitions/StringsWatchResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(BoolResults) async def AreManuallyProvisioned(self, entities): @@ -13053,7 +13053,7 @@ class KeyManagerFacade(Type): 'Result': {'$ref': '#/definitions/StringsResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def AddKeys(self, ssh_keys, user): @@ -13172,7 +13172,7 @@ class KeyUpdaterFacade(Type): 'Result': {'$ref': '#/definitions/NotifyWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(StringsResults) async def AuthorisedKeys(self, entities): @@ -13249,7 +13249,7 @@ class LeadershipServiceFacade(Type): 'Result': {'$ref': '#/definitions/ClaimLeadershipBulkResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResult) async def BlockUntilLeadershipReleased(self, name): @@ -13330,7 +13330,7 @@ class LifeFlagFacade(Type): 'Result': {'$ref': '#/definitions/NotifyWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(LifeResults) async def Life(self, entities): @@ -13426,7 +13426,7 @@ class LogForwardingFacade(Type): 'Result': {'$ref': '#/definitions/ErrorResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(LogForwardingGetLastSentResults) async def GetLastSent(self, ids): @@ -13507,7 +13507,7 @@ class LoggerFacade(Type): 'Result': {'$ref': '#/definitions/NotifyWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(StringResults) async def LoggingConfig(self, entities): @@ -13648,7 +13648,7 @@ class MachineActionsFacade(Type): 'Result': {'$ref': '#/definitions/StringsWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ActionResults) async def Actions(self, entities): @@ -13860,7 +13860,7 @@ class MachineManagerFacade(Type): 'Result': {'$ref': '#/definitions/InstanceTypesResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(AddMachinesResults) async def AddMachines(self, params): @@ -13968,7 +13968,7 @@ class MachineUndertakerFacade(Type): 'Result': {'$ref': '#/definitions/NotifyWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(EntitiesResults) async def AllMachineRemovals(self, entities): @@ -14243,7 +14243,7 @@ class MachinerFacade(Type): 'WatchAPIHostPorts': {'properties': {'Result': {'$ref': '#/definitions/NotifyWatchResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(StringsResult) async def APIAddresses(self): @@ -14507,7 +14507,7 @@ class MeterStatusFacade(Type): 'Result': {'$ref': '#/definitions/NotifyWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(MeterStatusResults) async def GetMeterStatus(self, entities): @@ -14593,7 +14593,7 @@ class MetricsAdderFacade(Type): 'Result': {'$ref': '#/definitions/ErrorResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def AddMetricBatches(self, batches): @@ -14676,7 +14676,7 @@ class MetricsDebugFacade(Type): 'Result': {'$ref': '#/definitions/ErrorResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(MetricResults) async def GetMetrics(self, entities): @@ -14745,7 +14745,7 @@ class MetricsManagerFacade(Type): 'Result': {'$ref': '#/definitions/ErrorResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def CleanupOldMetrics(self, entities): @@ -14825,7 +14825,7 @@ class MigrationFlagFacade(Type): 'Result': {'$ref': '#/definitions/NotifyWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(PhaseResults) async def Phase(self, entities): @@ -15028,7 +15028,7 @@ class MigrationMasterFacade(Type): 'WatchMinionReports': {'properties': {'Result': {'$ref': '#/definitions/NotifyWatchResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(SerializedModel) async def Export(self): @@ -15211,7 +15211,7 @@ class MigrationMinionFacade(Type): 'Watch': {'properties': {'Result': {'$ref': '#/definitions/NotifyWatchResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(None) async def Report(self, migration_id, phase, success): @@ -15273,7 +15273,7 @@ class MigrationStatusWatcherFacade(Type): 'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(MigrationStatus) async def Next(self): @@ -15408,7 +15408,7 @@ class MigrationTargetFacade(Type): 'Prechecks': {'properties': {'Params': {'$ref': '#/definitions/MigrationModelInfo'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(None) async def Abort(self, model_tag): @@ -15547,7 +15547,7 @@ class ModelConfigFacade(Type): 'ModelUnset': {'properties': {'Params': {'$ref': '#/definitions/ModelUnset'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ModelConfigResults) async def ModelGet(self): @@ -15861,7 +15861,7 @@ class ModelManagerFacade(Type): 'Result': {'$ref': '#/definitions/ErrorResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ModelInfo) async def CreateModel(self, cloud_tag, config, credential, name, owner_tag, region): @@ -16042,7 +16042,7 @@ class NotifyWatcherFacade(Type): version = 1 schema = {'properties': {'Next': {'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(None) async def Next(self): @@ -16107,7 +16107,7 @@ class PayloadsFacade(Type): 'Result': {'$ref': '#/definitions/EnvListResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(EnvListResults) async def List(self, patterns): @@ -16218,7 +16218,7 @@ class PayloadsHookContextFacade(Type): 'Result': {'$ref': '#/definitions/PayloadResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(PayloadResults) async def List(self, entities): @@ -16299,7 +16299,7 @@ class PingerFacade(Type): version = 1 schema = {'properties': {'Ping': {'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(None) async def Ping(self): @@ -16965,7 +16965,7 @@ class ProvisionerFacade(Type): 'WatchModelMachines': {'properties': {'Result': {'$ref': '#/definitions/StringsWatchResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(StringsResult) async def APIAddresses(self): @@ -17655,7 +17655,7 @@ class ProxyUpdaterFacade(Type): 'Result': {'$ref': '#/definitions/NotifyWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ProxyConfigResults) async def ProxyConfig(self, entities): @@ -17742,7 +17742,7 @@ class RebootFacade(Type): 'WatchForRebootEvent': {'properties': {'Result': {'$ref': '#/definitions/NotifyWatchResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def ClearReboot(self, entities): @@ -17839,7 +17839,7 @@ class RelationUnitsWatcherFacade(Type): 'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(RelationUnitsWatchResult) async def Next(self): @@ -17931,7 +17931,7 @@ class RemoteApplicationWatcherFacade(Type): 'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(RemoteApplicationWatchResult) async def Next(self): @@ -18015,7 +18015,7 @@ class RemoteRelationsWatcherFacade(Type): 'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(RemoteRelationsWatchResult) async def Next(self): @@ -18164,7 +18164,7 @@ class ResourcesFacade(Type): 'Result': {'$ref': '#/definitions/ResourcesResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(AddPendingResourcesResult) async def AddPendingResources(self, addcharmwithauthorization, entity, resources): @@ -18269,7 +18269,7 @@ class ResourcesHookContextFacade(Type): 'Result': {'$ref': '#/definitions/ResourcesResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ResourcesResult) async def GetResourceInfo(self, entities): @@ -18289,7 +18289,7 @@ class ResumerFacade(Type): name = 'Resumer' version = 2 schema = {'properties': {'ResumeTransactions': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(None) async def ResumeTransactions(self): @@ -18366,7 +18366,7 @@ class RetryStrategyFacade(Type): 'Result': {'$ref': '#/definitions/NotifyWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(RetryStrategyResults) async def RetryStrategy(self, entities): @@ -18469,7 +18469,7 @@ class SSHClientFacade(Type): 'Result': {'$ref': '#/definitions/SSHPublicKeysResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(SSHAddressesResults) async def AllAddresses(self, entities): @@ -18596,7 +18596,7 @@ class SingularFacade(Type): 'Result': {'$ref': '#/definitions/ErrorResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def Claim(self, claims): @@ -18697,7 +18697,7 @@ class SpacesFacade(Type): 'ListSpaces': {'properties': {'Result': {'$ref': '#/definitions/ListSpacesResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def CreateSpaces(self, spaces): @@ -18740,7 +18740,7 @@ class StatusHistoryFacade(Type): 'properties': {'Prune': {'properties': {'Params': {'$ref': '#/definitions/StatusHistoryPruneArgs'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(None) async def Prune(self, max_history_mb, max_history_time): @@ -18986,7 +18986,7 @@ class StorageFacade(Type): 'Result': {'$ref': '#/definitions/StorageDetailsResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def AddToUnit(self, storages): @@ -19517,7 +19517,7 @@ class StorageProvisionerFacade(Type): 'Result': {'$ref': '#/definitions/StringsWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(LifeResults) async def AttachmentLife(self, ids): @@ -19948,7 +19948,7 @@ class StringsWatcherFacade(Type): 'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(StringsWatchResult) async def Next(self): @@ -20070,7 +20070,7 @@ class SubnetsFacade(Type): 'Result': {'$ref': '#/definitions/ListSubnetsResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def AddSubnets(self, subnets): @@ -20220,7 +20220,7 @@ class UndertakerFacade(Type): 'WatchModelResources': {'properties': {'Result': {'$ref': '#/definitions/NotifyWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ModelConfigResult) async def ModelConfig(self): @@ -20390,7 +20390,7 @@ class UnitAssignerFacade(Type): 'WatchUnitAssignments': {'properties': {'Result': {'$ref': '#/definitions/StringsWatchResult'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(ErrorResults) async def AssignUnits(self, entities): @@ -21275,7 +21275,7 @@ class UniterFacade(Type): 'Result': {'$ref': '#/definitions/StringResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(StringsResult) async def APIAddresses(self): @@ -22441,7 +22441,7 @@ class UpgraderFacade(Type): 'Result': {'$ref': '#/definitions/NotifyWatchResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(VersionResults) async def DesiredVersion(self, entities): @@ -22617,7 +22617,7 @@ class UserManagerFacade(Type): 'Result': {'$ref': '#/definitions/UserInfoResults'}}, 'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(AddUserResults) async def AddUser(self, users): @@ -22742,7 +22742,7 @@ class VolumeAttachmentsWatcherFacade(Type): 'type': 'object'}, 'Stop': {'type': 'object'}}, 'type': 'object'} - + @ReturnMapping(MachineStorageIdsWatchResult) async def Next(self): @@ -22771,5 +22771,3 @@ class VolumeAttachmentsWatcherFacade(Type): reply = await self.rpc(msg) return reply - - diff --git a/juju/client/client.py b/juju/client/client.py index f4eef0e..5ea4c0e 100644 --- a/juju/client/client.py +++ b/juju/client/client.py @@ -1,5 +1,4 @@ '''Replace auto-generated classes with our own, where necessary. - ''' from . import _client diff --git a/juju/client/facade.py b/juju/client/facade.py index a7083e6..a6fd9f6 100644 --- a/juju/client/facade.py +++ b/juju/client/facade.py @@ -401,7 +401,11 @@ def _buildMethod(cls, name): prop = method['properties'] spec = prop.get('Params') if spec: - params = _types.get(spec['$ref']) + result = _types.get(spec['$ref']) + if '$ref' in spec: + result = _types.get(spec['$ref']) + else: + result = SCHEMA_TO_PYTHON[spec['type']] spec = prop.get('Result') if spec: if '$ref' in spec: diff --git a/juju/controller.py b/juju/controller.py index 135027a..98b3057 100644 --- a/juju/controller.py +++ b/juju/controller.py @@ -97,7 +97,7 @@ class Controller(object): credential, model_name, owner, - region, + region ) # Add our ssh key to the model, to work around @@ -152,7 +152,7 @@ class Controller(object): ]) destroy_model = destroy_models - def add_user(self, username, display_name=None, acl=None, models=None): + async def add_user(self, username, password=None, display_name=None): """Add a user to this controller. :param str username: Username @@ -161,39 +161,57 @@ class Controller(object): :param list models: Models to which the user is granted access """ - raise NotImplementedError() - - def change_user_password(self, username, password): + if not display_name: + display_name = username + user_facade = client.UserManagerFacade() + user_facade.connect(self.connection) + users = [{'display_name': display_name, + 'password': password, + 'username': username}] + return await user_facade.AddUser(users) + + async def change_user_password(self, username, password): """Change the password for a user in this controller. :param str username: Username :param str password: New password """ - raise NotImplementedError() + user_facade = client.UserManagerFacade() + user_facade.connect(self.connection) + entity = client.EntityPassword(password, tag.user(username)) + return await user_facade.SetPassword([entity]) - def destroy(self, destroy_all_models=False): + async def destroy(self, destroy_all_models=False): """Destroy this controller. :param bool destroy_all_models: Destroy all hosted models in the controller. """ - raise NotImplementedError() + controller_facade = client.ControllerFacade() + controller_facade.connect(self.connection) + return await controller_facade.DestroyController(destroy_all_models) - def disable_user(self, username): + async def disable_user(self, username): """Disable a user. :param str username: Username """ - raise NotImplementedError() + user_facade = client.UserManagerFacade() + user_facade.connect(self.connection) + entity = client.Entity(tag.user(username)) + return await user_facade.DisableUser([entity]) - def enable_user(self): + async def enable_user(self, username): """Re-enable a previously disabled user. """ - raise NotImplementedError() + user_facade = client.UserManagerFacade() + user_facade.connect(self.connection) + entity = client.Entity(tag.user(username)) + return await user_facade.EnableUser([entity]) def kill(self): """Forcibly terminate all machines and other associated resources for @@ -213,7 +231,7 @@ class Controller(object): cloud = list(result.clouds.keys())[0] # only lives on one cloud return tag.untag('cloud-', cloud) - def get_models(self, all_=False, username=None): + async def get_models(self, all_=False, username=None): """Return list of available models on this controller. :param bool all_: List all models, regardless of user accessibilty @@ -221,7 +239,10 @@ class Controller(object): :param str username: User for which to list models (admin use only) """ - raise NotImplementedError() + controller_facade = client.ControllerFacade() + controller_facade.connect(self.connection) + return await controller_facade.AllModels() + def get_payloads(self, *patterns): """Return list of known payloads. @@ -272,10 +293,39 @@ class Controller(object): """ raise NotImplementedError() - def get_user(self, username): + async def get_user(self, username, include_disabled=False): """Get a user by name. :param str username: Username """ - raise NotImplementedError() + client_facade = client.UserManagerFacade() + client_facade.connect(self.connection) + user = tag.user(username) + return await client_facade.UserInfo([client.Entity(user)], include_disabled) + + async def grant(self, username, acl='login'): + """Set access level of the given user on the controller + + :param str username: Username + :param str acl: Access control ('login', 'add-model' or 'superuser') + + """ + controller_facade = client.ControllerFacade() + controller_facade.connect(self.connection) + user = tag.user(username) + await self.revoke(username) + changes = client.ModifyControllerAccess(acl, 'grant', user) + return await controller_facade.ModifyControllerAccess([changes]) + + async def revoke(self, username): + """Removes all access from a controller + + :param str username: username + + """ + controller_facade = client.ControllerFacade() + controller_facade.connect(self.connection) + user = tag.user(username) + changes = client.ModifyControllerAccess('login', 'revoke', user) + return await controller_facade.ModifyControllerAccess([changes]) diff --git a/juju/model.py b/juju/model.py index c76ce88..6e236bf 100644 --- a/juju/model.py +++ b/juju/model.py @@ -1,5 +1,7 @@ import asyncio +import base64 import collections +import hashlib import json import logging import os @@ -849,13 +851,16 @@ class Model(object): """ raise NotImplementedError() - def add_ssh_key(self, key): + async def add_ssh_key(self, user, key): """Add a public SSH key to this model. + :param str user: The username of the user :param str key: The public ssh key """ - raise NotImplementedError() + key_facade = client.KeyManagerFacade() + key_facade.connect(self.connection) + return await key_facade.AddKeys([key], user) add_ssh_keys = add_ssh_key def add_subnet(self, cidr_or_id, space, *zones): @@ -1072,7 +1077,7 @@ class Model(object): storage=storage, channel=channel, num_units=num_units, - placement=parse_placement(to), + placement=parse_placement(to) ) async def _add_store_resources(self, application, entity_url, entity=None): @@ -1132,7 +1137,7 @@ class Model(object): num_units=num_units, resources=resources, storage=storage, - placement=placement, + placement=placement ) result = await app_facade.Deploy([app]) @@ -1141,9 +1146,9 @@ class Model(object): raise JujuError('\n'.join(errors)) return await self._wait_for_new('application', application) - def destroy(self): + async def destroy(self): """Terminate all machines and resources for this model. - + Is already implemented in controller.py. """ raise NotImplementedError() @@ -1202,14 +1207,21 @@ class Model(object): """ raise NotImplementedError() - def grant(self, username, acl='read'): + async def grant(self, username, acl='read'): """Grant a user access to this model. :param str username: Username :param str acl: Access control ('read' or 'write') """ - raise NotImplementedError() + model_facade = client.ModelManagerFacade() + controller_conn = await self.connection.controller() + model_facade.connect(controller_conn) + user = tag.user(username) + model = tag.model(self.info.uuid) + changes = client.ModifyModelAccess(acl, 'grant', model, user) + await self.revoke(username) + return await model_facade.ModifyModelAccess([changes]) def import_ssh_key(self, identity): """Add a public SSH key from a trusted indentity source to this model. @@ -1220,14 +1232,11 @@ class Model(object): raise NotImplementedError() import_ssh_keys = import_ssh_key - def get_machines(self, machine, utc=False): + async def get_machines(self): """Return list of machines in this model. - :param str machine: Machine id, e.g. '0' - :param bool utc: Display time as UTC in RFC3339 format - """ - raise NotImplementedError() + return list(self.state.machines.keys()) def get_shares(self): """Return list of all users with access to this model. @@ -1241,11 +1250,16 @@ class Model(object): """ raise NotImplementedError() - def get_ssh_key(self): + async def get_ssh_key(self, raw_ssh=False): """Return known SSH keys for this model. + :param bool raw_ssh: if True, returns the raw ssh key, else it's fingerprint """ - raise NotImplementedError() + key_facade = client.KeyManagerFacade() + key_facade.connect(self.connection) + entity = {'tag': tag.model(self.info.uuid)} + entities = client.Entities([entity]) + return await key_facade.ListKeys(entities, raw_ssh) get_ssh_keys = get_ssh_key def get_storage(self, filesystem=False, volume=False): @@ -1308,13 +1322,19 @@ class Model(object): raise NotImplementedError() remove_machines = remove_machine - def remove_ssh_key(self, *keys): + async def remove_ssh_key(self, user, key): """Remove a public SSH key(s) from this model. - :param str \*keys: Keys to remove + :param str key: Full ssh key + :param str user: Juju user to which the key is registered """ - raise NotImplementedError() + key_facade = client.KeyManagerFacade() + key_facade.connect(self.connection) + key = base64.b64decode(bytes(key.strip().split()[1].encode('ascii'))) + key = hashlib.md5(key).hexdigest() + key = ':'.join(a+b for a, b in zip(key[::2], key[1::2])) + await key_facade.DeleteKeys([key], user) remove_ssh_keys = remove_ssh_key def restore_backup( @@ -1338,14 +1358,19 @@ class Model(object): """ raise NotImplementedError() - def revoke(self, username, acl='read'): + async def revoke(self, username): """Revoke a user's access to this model. :param str username: Username to revoke - :param str acl: Access control ('read' or 'write') """ - raise NotImplementedError() + model_facade = client.ModelManagerFacade() + controller_conn = await self.connection.controller() + model_facade.connect(controller_conn) + user = tag.user(username) + model = tag.model(self.info.uuid) + changes = client.ModifyModelAccess('read', 'revoke', model, user) + return await model_facade.ModifyModelAccess([changes]) def run(self, command, timeout=None): """Run command on all machines in this model. diff --git a/tests/base.py b/tests/base.py index 292d04a..8ea5109 100644 --- a/tests/base.py +++ b/tests/base.py @@ -19,6 +19,19 @@ bootstrapped = pytest.mark.skipif( reason='bootstrapped Juju environment required') +class CleanController(): + def __init__(self): + self.controller = None + + async def __aenter__(self): + self.controller = Controller() + await self.controller.connect_current() + return self.controller + + async def __aexit__(self, exc_type, exc, tb): + await self.controller.disconnect() + + class CleanModel(): def __init__(self): self.controller = None diff --git a/tests/charm/metadata.yaml b/tests/charm/metadata.yaml new file mode 100644 index 0000000..74eab3d --- /dev/null +++ b/tests/charm/metadata.yaml @@ -0,0 +1,5 @@ +name: charm +series: ["xenial"] +summary: "test" +description: "test" +maintainers: ["test"] diff --git a/tests/integration/test_controller.py b/tests/integration/test_controller.py new file mode 100644 index 0000000..c334389 --- /dev/null +++ b/tests/integration/test_controller.py @@ -0,0 +1,69 @@ +import asyncio +from concurrent.futures import ThreadPoolExecutor +import pytest + +from .. import base +from juju.controller import Controller +from juju.errors import JujuAPIError + + +@base.bootstrapped +@pytest.mark.asyncio +async def test_add_user(event_loop): + async with base.CleanController() as controller: + await controller.add_user('test') + result = await controller.get_user('test') + res_ser = result.serialize()['results'][0].serialize() + assert res_ser['result'] is not None + + +@base.bootstrapped +@pytest.mark.asyncio +async def test_disable_enable_user(event_loop): + async with base.CleanController() as controller: + await controller.add_user('test-disable') + await controller.disable_user('test-disable') + result = await controller.get_user('test-disable') + res_ser = result.serialize()['results'][0].serialize() + assert res_ser['result'].serialize()['disabled'] is True + await controller.enable_user('test-disable') + result = await controller.get_user('test-disable') + res_ser = result.serialize()['results'][0].serialize() + assert res_ser['result'].serialize()['disabled'] is False + + +@base.bootstrapped +@pytest.mark.asyncio +async def test_change_user_password(event_loop): + async with base.CleanController() as controller: + await controller.add_user('test-password') + await controller.change_user_password('test-password', 'password') + try: + con = await controller.connect(controller.connection.endpoint, 'test-password', 'password') + result = True + except JujuAPIError: + result = False + assert result is True + + +@base.bootstrapped +@pytest.mark.asyncio +async def test_grant(event_loop): + async with base.CleanController() as controller: + await controller.add_user('test-grant') + await controller.grant('test-grant', 'superuser') + result = await controller.get_user('test-grant') + result = result.serialize()['results'][0].serialize()['result'].serialize() + assert result['access'] == 'superuser' + await controller.grant('test-grant', 'login') + result = await controller.get_user('test-grant') + result = result.serialize()['results'][0].serialize()['result'].serialize() + assert result['access'] == 'login' + + +@base.bootstrapped +@pytest.mark.asyncio +async def test_get_models(event_loop): + async with base.CleanController() as controller: + result = await controller.get_models() + assert isinstance(result.serialize()['user-models'], list) diff --git a/tests/integration/test_model.py b/tests/integration/test_model.py index 96c786a..4aec314 100644 --- a/tests/integration/test_model.py +++ b/tests/integration/test_model.py @@ -8,7 +8,7 @@ from juju.model import Model MB = 1 GB = 1024 - +SSH_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsYMJGNGG74HAJha3n2CFmWYsOOaORnJK6VqNy86pj0MIpvRXBzFzVy09uPQ66GOQhTEoJHEqE77VMui7+62AcMXT+GG7cFHcnU8XVQsGM6UirCcNyWNysfiEMoAdZScJf/GvoY87tMEszhZIUV37z8PUBx6twIqMdr31W1J0IaPa+sV6FEDadeLaNTvancDcHK1zuKsL39jzAg7+LYjKJfEfrsQP+lj/EQcjtKqlhVS5kzsJVfx8ZEd0xhW5G7N6bCdKNalS8mKCMaBXJpijNQ82AiyqCIDCRrre2To0/i7pTjRiL0U9f9mV3S4NJaQaokR050w/ZLySFf6F7joJT mathijs@Qrama-Mathijs' @base.bootstrapped @pytest.mark.asyncio @@ -172,3 +172,38 @@ async def test_store_resources_bundle(event_loop): # ghost will go in to blocked (or error, for older # charm revs) if the resource is missing assert ghost.units[0].workload_status == 'active' + + +@base.bootstrapped +@pytest.mark.asyncio +async def test_ssh_key(event_loop): + async with base.CleanModel() as model: + await model.add_ssh_key('admin', SSH_KEY) + result = await model.get_ssh_key(True) + result = result.serialize()['results'][0].serialize()['result'] + assert SSH_KEY in result + await model.remove_ssh_key('admin', SSH_KEY) + result = await model.get_ssh_key(True) + result = result.serialize()['results'][0].serialize()['result'] + assert result is None + + +@base.bootstrapped +@pytest.mark.asyncio +async def test_get_machines(event_loop): + async with base.CleanModel() as model: + result = await model.get_machines() + assert isinstance(result, list) + + +# @base.bootstrapped +# @pytest.mark.asyncio +# async def test_grant(event_loop) +# async with base.CleanController() as controller: +# await controller.add_user('test-model-grant') +# await controller.grant('test-model-grant', 'superuser') +# async with base.CleanModel() as model: +# await model.grant('test-model-grant', 'admin') +# assert model.get_user('test-model-grant')['access'] == 'admin' +# await model.grant('test-model-grant', 'login') +# assert model.get_user('test-model-grant')['access'] == 'login' -- 2.17.1