diff --git a/README.rst b/README.rst index 52744cf..bb01fba 100644 --- a/README.rst +++ b/README.rst @@ -13,8 +13,9 @@ for Nova (key pairs and servers), Cinder (volumes) and Neutron (router, networks, subnets, security groups and floating IPs) resources. `flame` works as follows: using provided credentials (user name, project name, -password, authentication url), the tool will list supported resources deployed -in the project and will generate corresponding, highly customized HOT template. +password or auth_token, authentication url), the tool will list supported +resources deployed in the project and will generate corresponding, highly +customized HOT template. Installation ------------ @@ -46,6 +47,8 @@ Usage --project PROJECT Name of project. Defaults to env[OS_TENANT_NAME] --region REGION Name of region. Defaults to env[OS_REGION_NAME] --auth_url AUTH_URL Authentication URL. Defaults to env[OS_AUTH_URL]. + --os-auth-token OS_AUTH_TOKEN + User's auth token. Defaults to env[OS_AUTH_TOKEN]. --insecure Explicitly allow clients to perform"insecure" SSL (https) requests. The server's certificate will not be verified against any certificate authorities. This @@ -67,3 +70,7 @@ To use Flame you can provide yours OpenStack credentials as arguments : Or you can source your OpenStack RC file and use Flame without arguments. + +Flame can be used with either a login and password pair or a keystone +token by exporting the OS_AUTH_TOKEN variable (the token is obtained +with keystone token-get). diff --git a/doc/source/usage.rst b/doc/source/usage.rst index de7640e..8eca476 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -9,8 +9,9 @@ To use install flame in a project:: To use the CLI of flame:: usage: flame [-h] [--username USERNAME] [--password PASSWORD] - [--project PROJECT] [--auth_url AUTH_URL] [--insecure] - [--exclude_servers] [--exclude_volumes] + [--project PROJECT] [--os-auth-token OS_AUTH_TOKEN] + [--auth_url AUTH_URL] [--insecure] [--exclude_servers] + [--exclude_volumes] Generate Heat Template @@ -21,6 +22,8 @@ To use the CLI of flame:: --password PASSWORD The user's password. Defaults to env[OS_PASSWORD] --project PROJECT Name of project. Defaults to env[OS_TENANT_NAME] --auth_url AUTH_URL Authentication URL. Defaults to env[OS_AUTH_URL]. + --os-auth-token OS_AUTH_TOKEN + User's auth token. Defaults to env[OS_AUTH_TOKEN]. --insecure Explicitly allow clients to perform"insecure" SSL (https) requests. The server's certificate will not be verified against any certificate authorities. This @@ -30,7 +33,7 @@ To use the CLI of flame:: --generate-stack-data In addition to template, generate Heat stack data file. - + Example ------- @@ -40,6 +43,11 @@ To use Flame you can provide yours OpenStack credentials as arguments:: $ flame --username arezmerita --password password \ --project project-arezmerita --auth_url https://example.com/v2.0/ +Or a token and a tenant:: + + $ flame --username arezmerita --os-auth-token keystonetoken \ + --project project-arezmerita --auth_url https://example.com/v2.0/ + Or you can source your OpenStack RC file and use Flame without arguments:: $ source credential.rc diff --git a/flameclient/client.py b/flameclient/client.py index fbaf5a9..cd020b8 100644 --- a/flameclient/client.py +++ b/flameclient/client.py @@ -26,12 +26,24 @@ from flameclient.flame import TemplateGenerator # noqa class Client(object): - def __init__(self, username, password, tenant_name, auth_url, **kwargs): + def __init__(self, username, password, tenant_name, auth_url, auth_token, + **kwargs): self.template_generator = TemplateGenerator(username, password, tenant_name, auth_url, + auth_token, **kwargs) - def generate(self, include_networks, include_instances, include_volumes): - return self.template_generator.generate(include_networks, - include_instances, - include_volumes) + def generate(self, exclude_servers, exclude_volumes, exclude_keypairs, + generate_stack_data): + self.template_generator.extract_vm_details(exclude_servers, + exclude_volumes, + exclude_keypairs, + generate_stack_data + ) + self.template_generator.extract_data() + heat_template = self.template_generator.heat_template() + if generate_stack_data: + stack_data = self.template_generator.stack_data_template() + else: + stack_data = None + return (heat_template, stack_data) diff --git a/flameclient/cmd.py b/flameclient/cmd.py index 39dcca1..447bcb0 100644 --- a/flameclient/cmd.py +++ b/flameclient/cmd.py @@ -53,6 +53,10 @@ def main(args=None): default=os.environ.get("OS_AUTH_URL"), help="Authentication URL. " "Defaults to env[OS_AUTH_URL].") + parser.add_argument("--os-auth-token", type=str, + default=os.environ.get("OS_AUTH_TOKEN"), + help="User's auth token. " + "Defaults to env[OS_AUTH_TOKEN].") parser.add_argument('--insecure', action='store_true', default=False, help="Explicitly allow clients to perform" "\"insecure\" SSL (https) requests. The " @@ -80,9 +84,10 @@ def main(args=None): args = parser.parse_args() flame = client.Client(args.username, args.password, args.project, args.auth_url, - insecure=args.insecure, + args.os_auth_token, + region_name=args.region, endpoint_type=args.endpoint_type, - region_name=args.region) + insecure=args.insecure) template = flame.template_generator template.extract_vm_details(args.exclude_servers, args.exclude_volumes, diff --git a/flameclient/flame.py b/flameclient/flame.py index ad0611c..61eb45c 100644 --- a/flameclient/flame.py +++ b/flameclient/flame.py @@ -105,11 +105,12 @@ class Resource(object): class TemplateGenerator(object): def __init__(self, username, password, tenant_name, auth_url, - insecure=False, endpoint_type='publicURL', region_name=None): + auth_token=None, insecure=False, endpoint_type='publicURL', + region_name=None): self.generate_data = False self._setup_templates() self._setup_managers(username, password, tenant_name, auth_url, - insecure, endpoint_type, region_name=region_name) + insecure, endpoint_type, region_name, auth_token) def _setup_templates(self): self.template = yaml.load(template_skeleton) @@ -120,19 +121,20 @@ class TemplateGenerator(object): self.stack_data['resources'] = {} def _setup_managers(self, username, password, tenant_name, auth_url, - insecure, endpoint_type, region_name=None): - self.neutron = managers.NeutronManager(username, password, tenant_name, - auth_url, insecure, - endpoint_type, - region_name=region_name) - self.nova = managers.NovaManager(username, password, tenant_name, - auth_url, insecure, - endpoint_type, - region_name=region_name) - self.cinder = managers.CinderManager(username, password, tenant_name, - auth_url, insecure, - endpoint_type, - region_name=region_name) + insecure, endpoint_type, region_name=None, + auth_token=None): + self.keystone = managers.KeystoneManager( + username, password, + tenant_name, + auth_url, insecure, + endpoint_type, + region_name=region_name, + auth_token=auth_token + ) + self.keystone.authenticate() + self.neutron = managers.NeutronManager(self.keystone) + self.nova = managers.NovaManager(self.keystone) + self.cinder = managers.CinderManager(self.keystone) def extract_vm_details(self, exclude_servers, exclude_volumes, exclude_keypairs, generate_data): @@ -523,10 +525,13 @@ class TemplateGenerator(object): "Snapshot to create volume %s from" % resource_name) resource.add_parameter(key, description, default=volume.snapshot_id) - if volume.display_name: - properties['name'] = volume.display_name - if volume.display_description: - properties['description'] = volume.display_description + try: + if volume.display_name: + properties['name'] = volume.display_name + if volume.display_description: + properties['description'] = volume.display_description + except AttributeError: + pass if volume.volume_type and volume.volume_type != 'None': key = "%s_volume_type" % resource_name description = ( diff --git a/flameclient/managers.py b/flameclient/managers.py index 3d1e5ab..cbeca25 100644 --- a/flameclient/managers.py +++ b/flameclient/managers.py @@ -23,7 +23,6 @@ # SOFTWARE. from cinderclient.v1 import client as cinder_client -from keystoneclient import exceptions as keystone_exceptions from keystoneclient.v2_0 import client as keystone_client from neutronclient.v2_0 import client as neutron_client from novaclient import client as nova_client @@ -34,7 +33,7 @@ class KeystoneManager(object): _client = None def __init__(self, username, password, project, auth_url, insecure, - endpoint_type='publicURL', region_name=None): + endpoint_type='publicURL', region_name=None, auth_token=None): self.username = username self.password = password self.project = project @@ -42,6 +41,11 @@ class KeystoneManager(object): self.insecure = insecure self.region_name = region_name self.endpoint_type = endpoint_type + self.auth_token = auth_token + + def authenticate(self): + self.client().authenticate() + self.auth_token = self.client().auth_token def client(self): if not self._client: @@ -52,7 +56,8 @@ class KeystoneManager(object): auth_url=self.auth_url, region_name=self.region_name, insecure=self.insecure, - endpoint_type=self.endpoint_type) + endpoint_type=self.endpoint_type, + token=self.auth_token) return self._client def set_client(self, client): @@ -73,32 +78,19 @@ class NeutronManager(object): _client = None _project_id = None - def __init__(self, username, password, project, auth_url, insecure, - endpoint_type='publicURL', region_name=None): - self.username = username - self.password = password - self.project = project - self.auth_url = auth_url - self.insecure = insecure - self.endpoint_type = endpoint_type - self.region_name = region_name + def __init__(self, keystone_mgr): + self.keystone_mgr = keystone_mgr def client(self): if not self._client: + # Create the client self._client = neutron_client.Client( - username=self.username, - password=self.password, - tenant_name=self.project, - auth_url=self.auth_url, - region_name=self.region_name, - insecure=self.insecure, - endpoint_type=self.endpoint_type) + auth_url=self.keystone_mgr.auth_url, + insecure=self.keystone_mgr.insecure, + endpoint_url=self.keystone_mgr.get_endpoint('network'), + token=self.keystone_mgr.auth_token) if not self._project_id: - keystone_mgr = KeystoneManager(self.username, self.password, - self.project, self.auth_url, - self.insecure, - region_name=self.region_name) - self._project_id = keystone_mgr.get_project_id() + self._project_id = self.keystone_mgr.get_project_id() return self._client def set_client(self, client): @@ -142,24 +134,22 @@ class NovaManager(object): """Manage nova resources.""" _client = None - def __init__(self, username, password, project, auth_url, insecure, - endpoint_type='publicURL', region_name=None): - self.username = username - self.password = password - self.project = project - self.auth_url = auth_url - self.region_name = region_name - self.insecure = insecure - self.endpoint_type = endpoint_type + def __init__(self, keystone_mgr): + self.keystone_mgr = keystone_mgr def client(self): if not self._client: - self._client = nova_client.Client('2', - self.username, self.password, - self.project, self.auth_url, - region_name=self.region_name, - insecure=self.insecure, - endpoint_type=self.endpoint_type) + self._client = nova_client.Client( + '2', + self.keystone_mgr.username, + self.keystone_mgr.auth_token, + self.keystone_mgr.project, + self.keystone_mgr.auth_url, + region_name=self.keystone_mgr.region_name, + insecure=self.keystone_mgr.insecure, + endpoint_type=self.keystone_mgr.endpoint_type, + auth_token=self.keystone_mgr.auth_token + ) return self._client def set_client(self, client): @@ -191,35 +181,27 @@ class CinderManager(object): """Manage Cinder resources.""" _client = None - def __init__(self, username, password, project, auth_url, insecure, - endpoint_type='publicURL', region_name=None): - self.username = username - self.password = password - self.project = project - self.auth_url = auth_url - self.region_name = region_name - self.insecure = insecure + def __init__(self, keystone_mgr): + self.keystone_mgr = keystone_mgr self.defined = True - self.endpoint_type = endpoint_type def client(self): if self.defined and not self._client: - client = cinder_client.Client(self.username, - self.password, - self.project, - self.auth_url, - region_name=self.region_name, - insecure=self.insecure, - endpoint_type=self.endpoint_type) - - # Check cinder endpoint existence try: - client.authenticate() - self._client = client - except keystone_exceptions.EndpointNotFound: - self.defined = False - self._client = None - + cinder_url = self.keystone_mgr.get_endpoint("volumev2") + except KeyError: + cinder_url = self.keystone_mgr.get_endpoint("volume") + client = cinder_client.Client( + self.keystone_mgr.username, + self.keystone_mgr.auth_token, + project_id=self.keystone_mgr.project, + auth_url=cinder_url, + http_log_debug=True, + insecure=self.keystone_mgr.insecure + ) + client.client.auth_token = self.keystone_mgr.auth_token + client.client.management_url = cinder_url + self._client = client return self._client def set_client(self, client): diff --git a/flameclient/tests/test_flame.py b/flameclient/tests/test_flame.py index 08391d4..92e8192 100644 --- a/flameclient/tests/test_flame.py +++ b/flameclient/tests/test_flame.py @@ -24,6 +24,7 @@ import mock +from flameclient import client as flame_client from flameclient import flame from flameclient.tests import base @@ -238,11 +239,16 @@ class BaseTestCase(base.TestCase): self.mock_nova = self.patch_nova.start() self.patch_cinder = mock.patch('flameclient.managers.CinderManager') self.mock_cinder = self.patch_cinder.start() + self.patch_keystone = mock.patch( + 'flameclient.managers.KeystoneManager' + ) + self.mock_keystone = self.patch_keystone.start() def tearDown(self): self.mock_neutron.stop() self.mock_nova.stop() self.mock_cinder.stop() + self.mock_keystone.stop() super(BaseTestCase, self).tearDown() def get_generator(self, exclude_servers, exclude_volumes, @@ -274,6 +280,32 @@ class BaseTestCase(base.TestCase): self.assertEqual(expected_parameters, merged_parameters) +class ClientTest(BaseTestCase): + def setUp(self): + super(ClientTest, self).setUp() + self.c = flame_client.Client('username', 'password', 'tenant_name', + 'authUrl', 'auth_token') + + def test_generate(self): + out = self.c.generate(False, False, False, True) + self.assertIsInstance(out, tuple) + self.assertIsNotNone(out[1]) + + def test_generate_no_stack_data(self): + out = self.c.generate(False, False, False, False) + self.assertIsInstance(out, tuple) + self.assertIsNotNone(out[0]) + self.assertIsNone(out[1]) + + def test_generate_contains_extract(self): + generator = self.get_generator(False, False, False, True) + out = self.c.generate(False, False, False, True) + generator.extract_data() + stack_data = generator.stack_data_template() + heat_template = generator.heat_template() + self.assertEqual(out, (heat_template, stack_data)) + + class StackDataTests(BaseTestCase): def test_keypair(self):