Expanding controller.py with basic user functions, get_models and destroy (#89)
authorMathijs Moerman <mathijs.moerman@gmail.com>
Thu, 6 Apr 2017 17:58:12 +0000 (19:58 +0200)
committerCory Johns <johnsca@gmail.com>
Thu, 6 Apr 2017 17:58:12 +0000 (13:58 -0400)
* 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
juju/client/_client.py
juju/client/client.py
juju/client/facade.py
juju/controller.py
juju/model.py
tests/base.py
tests/charm/metadata.yaml [new file with mode: 0644]
tests/integration/test_controller.py [new file with mode: 0644]
tests/integration/test_model.py

index da5be27..866a785 100644 (file)
@@ -9,3 +9,4 @@ __pycache__/
 .cache/
 .\#*
 dist/
+dev/
index 0847da6..9746815 100644 (file)
@@ -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
-
-
index f4eef0e..5ea4c0e 100644 (file)
@@ -1,5 +1,4 @@
 '''Replace auto-generated classes with our own, where necessary.
-
 '''
 
 from . import _client
index a7083e6..a6fd9f6 100644 (file)
@@ -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:
index 135027a..98b3057 100644 (file)
@@ -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])
index c76ce88..6e236bf 100644 (file)
@@ -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.
index 292d04a..8ea5109 100644 (file)
@@ -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 (file)
index 0000000..74eab3d
--- /dev/null
@@ -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 (file)
index 0000000..c334389
--- /dev/null
@@ -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)
index 96c786a..4aec314 100644 (file)
@@ -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'