4 # Copyright 2016 RIFT.IO Inc
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
19 # @file test_openstack_install.py
20 # @author Varun Prasad (varun.prasad@riftio.com)
22 # @brief Test Openstack/os install
33 from keystoneclient
.v3
import client
39 from gi
.repository
import RwcalYang
40 from gi
.repository
.RwTypes
import RwStatus
45 logger
= logging
.getLogger()
46 logging
.basicConfig(level
=logging
.INFO
)
50 """A wrapper on top of a host, which provides a ssh connection instance.
53 The username/password for the VM is default.
58 def __init__(self
, hostname
):
61 hostname (str): Hostname (grunt3.qanet.riftio.com)
63 self
.hostname
= hostname
65 self
.ip
= socket
.gethostbyname(hostname
)
66 except socket
.gaierror
:
67 logger
.error("Unable to resolve the hostname {}".format(hostname
))
70 self
.ssh
= paramiko
.SSHClient()
71 # Note: Do not load the system keys as the test will fail if the keys
73 self
.ssh
.set_missing_host_key_policy(paramiko
.WarningPolicy())
76 """Set up ssh connection.
78 logger
.debug("Trying to connect to {}: {}".format(
84 username
=self
._USERNAME
,
85 password
=self
._PASSWORD
)
87 def put(self
, content
, dest
):
88 """Creates a tempfile and puts it in the destination path in the HOST.
90 content (str): Content to be written to a file.
91 dest (str): Path to store the content.
93 temp_file
= tempfile
.NamedTemporaryFile(delete
=False)
94 temp_file
.write(content
.encode("UTF-8"))
97 logger
.info("Writing {} file in {}".format(dest
, self
.hostname
))
98 sftp
= self
.ssh
.open_sftp()
99 sftp
.put(temp_file
.name
, dest
)
109 """A wrapper on top of grunt machine, provides functionalities to check
110 if the grunt is up, IP resolution.
113 def grunt_name(self
):
114 """Extract the grunt name from the FQDN
117 str: e.g. grunt3 from grunt3.qanet.riftio.com
119 return self
.hostname
.split(".")[0]
122 def dns_server(self
):
123 """Hard-coded for now.
128 def floating_ip(self
):
132 def private_ip(self
):
133 """Construct the private IP from the grunt name. 10.0.xx.0 where xx is
134 value of the grunt (3 in case of grunt3)
136 host_part
= re
.sub(r
"[a-zA-z]+", "", self
.grunt_name
)
137 return '10.0.{}.0'.format(host_part
)
139 def is_system_up(self
):
140 """Checks if system is up using ssh login.
143 bool: Indicates if system is UP
152 def wait_till_system_is_up(self
, timeout
=50, check_openstack
=False):
153 """Blocking call to check if system is up.
155 timeout (int, optional): In mins(~).
156 check_openstack (bool, optional): If true will also check if
157 openstack is up and running on the system.
160 OSError: If system start exceeds the timeout
163 TRY_DURATION
= 20 # secs
164 total_tries
= timeout
* (60 / TRY_DURATION
) # 3 tries/mins i.e. 20 secs.
167 while tries
< total_tries
:
168 if self
.is_system_up():
169 if check_openstack
and self
.is_openstack_up():
171 elif not check_openstack
:
174 logger
.info("{} down: Sleeping for {} secs. Try {} of {}".format(
180 time
.sleep(TRY_DURATION
)
183 raise OSError("Exception in system start {}({})".format(
187 def is_openstack_up(self
):
188 """Checks if openstack is UP, by verifying the URL.
191 bool: Indicates if system is UP
193 url
= "http://{}/dashboard/".format(self
.ip
)
195 logger
.info("Checking if openstack({}) is UP".format(url
))
199 except requests
.ConnectionError
:
206 """A thin wrapper on cobbler and provides an interface using XML rpc client.
209 System instances are already added to cobbler(with ipmi). Adding instances
210 can also be automated, can be taken up sometime later.
212 def __init__(self
, hostname
, username
="cobbler", password
="cobbler"):
215 hostname (str): Cobbler host.
216 username (str, optional): username.
217 password (str, optional): password
219 super().__init
__(hostname
)
221 url
= "https://{}/cobbler_api".format(hostname
)
223 self
.server
= xmlrpc
.client
.ServerProxy(url
)
224 logger
.info("obtained a cobbler instance for the host {}".format(hostname
))
226 self
.token
= self
.server
.login(username
, password
)
229 def create_profile(self
, profile_name
, ks_file
):
230 """Create the profile for the system.
233 profile_name (str): Name of the profile.
234 ks_file (str): Path of the kick start file.
237 "name": profile_name
,
238 "kickstart": ks_file
,
239 "repos": ['riftware', 'rift-misc', 'fc21-x86_64-updates',
240 'fc21-x86_64', 'openstack-kilo'],
242 "distro": "FC21.3-x86_64"
245 profile_id
= self
.server
.new_profile(self
.token
)
246 for key
, value
in profile_attrs
.items():
247 self
.server
.modify_profile(profile_id
, key
, value
, self
.token
)
248 self
.server
.save_profile(profile_id
, self
.token
)
250 def create_snippet(self
, snippet_name
, snippet_content
):
251 """Unfortunately the XML rpc apis don't provide a direct interface to
252 create snippets, so falling back on the default sftp methods.
255 snippet_name (str): Name.
256 snippet_content (str): snippet's content.
259 str: path where the snippet is stored
261 path
= "/var/lib/cobbler/snippets/{}".format(snippet_name
)
262 self
.put(snippet_content
, path
)
265 def create_kickstart(self
, ks_name
, ks_content
):
266 """Creates and returns the path of the ks file.
269 ks_name (str): Name of the ks file to be saved.
270 ks_content (str): Content for ks file.
273 str: path where the ks file is saved.
275 path
= "/var/lib/cobbler/kickstarts/{}".format(ks_name
)
276 self
.put(ks_content
, path
)
279 def boot_system(self
, grunt
, profile_name
, false_boot
=False):
280 """Boots the system with the profile specified. Also enable net-boot
283 grunt (Grunt): instance of grunt
284 profile_name (str): A valid profile name.
285 false_boot (bool, optional): debug only option.
290 system_id
= self
.server
.get_system_handle(
293 self
.server
.modify_system(
299 self
.server
.modify_system(
304 self
.server
.save_system(system_id
, self
.token
)
305 self
.server
.power_system(system_id
, "reboot", self
.token
)
308 class OpenstackTest(object):
309 """Driver class to automate the installation.
316 test_prefix
="openstack_test"):
319 cobbler (Cobbler): Instance of Cobbler
320 controller (Controller): Controller node instance
321 compute_nodes (TYPE, optional): A list of Grunt nodes to be set up
323 test_prefix (str, optional): All entities created by the script are
324 prefixed with this string.
326 self
.cobbler
= cobbler
327 self
.controller
= controller
328 self
.compute_nodes
= [] if compute_nodes
is None else compute_nodes
329 self
.test_prefix
= test_prefix
331 def _prepare_snippet(self
):
332 """Prepares the config based on the controller and compute nodes.
335 str: Openstack config content.
340 config
['host_name'] = self
.controller
.grunt_name
341 config
['ip'] = self
.controller
.ip
342 config
['dns_server'] = self
.controller
.dns_server
343 config
['private_ip'] = self
.controller
.private_ip
344 config
['floating_ip'] = self
.controller
.floating_ip
346 content
+= Template
.GRUNT_CONFIG
.format(**config
)
347 for compute_node
in self
.compute_nodes
:
348 config
["host_name"] = compute_node
.grunt_name
349 content
+= Template
.GRUNT_CONFIG
.format(**config
)
351 content
= Template
.SNIPPET_TEMPLATE
.format(config
=content
)
355 def prepare_profile(self
):
356 """Creates the cobbler profile.
358 snippet_content
= self
._prepare
_snippet
()
359 self
.cobbler
.create_snippet(
360 "{}.cfg".format(self
.test_prefix
),
363 ks_content
= Template
.KS_TEMPATE
364 ks_file
= self
.cobbler
.create_kickstart(
365 "{}.ks".format(self
.test_prefix
),
368 self
.cobbler
.create_profile(self
.test_prefix
, ks_file
)
369 return self
.test_prefix
371 def _get_cal_account(self
):
373 Creates an object for class RwcalYang.CloudAccount()
375 account
= RwcalYang
.CloudAccount()
376 account
.account_type
= "openstack"
377 account
.openstack
.key
= "{}_user".format(self
.test_prefix
)
378 account
.openstack
.secret
= "mypasswd"
379 account
.openstack
.auth_url
= 'http://{}:35357/v3/'.format(self
.controller
.ip
)
380 account
.openstack
.tenant
= self
.test_prefix
385 """Starts the installation.
387 profile_name
= self
.prepare_profile()
389 self
.cobbler
.boot_system(self
.controller
, profile_name
)
390 self
.controller
.wait_till_system_is_up(check_openstack
=True)
393 logger
.info("Controller system is UP. Setting up compute nodes")
394 for compute_node
in self
.compute_nodes
:
395 self
.cobbler
.boot_system(compute_node
, profile_name
)
396 compute_node
.wait_till_system_is_up()
398 logger
.error("System set-up failed {}".format(e
))
401 # Currently we don't have wrapper on top of users/projects so using
402 # keystone API directly
403 acct
= self
._get
_cal
_account
()
405 keystone_conn
= client
.Client(
406 auth_url
=acct
.openstack
.auth_url
,
410 # Create a test project
411 project
= keystone_conn
.projects
.create(
412 acct
.openstack
.tenant
,
414 description
="Openstack test project")
417 user
= keystone_conn
.users
.create(
419 password
=acct
.openstack
.secret
,
420 default_project
=project
)
422 # Make the newly created user as ADMIN
423 admin_role
= keystone_conn
.roles
.list(name
="admin")[0]
424 keystone_conn
.roles
.grant(
429 # nova API needs to be restarted, otherwise the new service doesn't play
431 self
.controller
.ssh
.exec_command("source keystonerc_admin && "
432 "service openstack-nova-api restart")
438 """Close out all SFTP connections.
440 nodes
= [self
.controller
]
441 nodes
.extend(self
.compute_nodes
)
446 ###############################################################################
448 ###############################################################################
451 @pytest.fixture(scope
="session")
454 Loads rw.cal plugin via libpeas
456 plugin
= rw_peas
.PeasPlugin('rwcal_openstack', 'RwCal-1.0')
457 engine
, info
, extension
= plugin()
459 # Get the RwLogger context
460 rwloggerctx
= rwlogger
.RwLog
.Ctx
.new("Cal-Log")
462 cal
= plugin
.get_interface("Cloud")
464 rc
= cal
.init(rwloggerctx
)
465 assert rc
== RwStatus
.SUCCESS
467 logger
.error("ERROR:Cal plugin instantiation failed. Aborting tests")
469 logger
.info("Openstack Cal plugin successfully instantiated")
474 @pytest.fixture(scope
="session")
475 def account(request
):
476 """Creates an openstack instance with 1 compute node and returns the newly
479 cobbler
= Cobbler("qacobbler.eng.riftio.com")
480 controller
= Grunt("grunt3.qanet.riftio.com")
481 compute_nodes
= [Grunt("grunt5.qanet.riftio.com")]
483 test
= OpenstackTest(cobbler
, controller
, compute_nodes
)
484 account
= test
.start()
486 request
.addfinalizer(test
.clear
)
490 def test_list_images(cal
, account
):
491 """Verify if 2 images are present
493 status
, resources
= cal
.get_image_list(account
)
494 assert len(resources
.imageinfo_list
) == 2
496 def test_list_flavors(cal
, account
):
497 """Basic flavor checks
499 status
, resources
= cal
.get_flavor_list(account
)
500 assert len(resources
.flavorinfo_list
) == 5
503 class Template(object):
504 """A container to hold all cobbler related templates.
515 PRIVATE_IP={private_ip}
516 FLOATING_IP={floating_ip}
517 DNS_SERVER={dns_server}
522 SNIPPET_TEMPLATE
= """
523 # =====================Begining of snippet=================
524 # snippet openstack_test.cfg
533 # =====================End of snippet=================
538 $SNIPPET('rift-repos')
539 $SNIPPET('rift-base')
543 $SNIPPET('rift-grunt-fc21-packages')
549 $SNIPPET('log_ks_pre')
550 $SNIPPET('kickstart_start')
551 # Enable installation monitoring
552 $SNIPPET('pre_anamon')
555 %post --log=/root/ks_post.log
556 $SNIPPET('openstack_test.cfg')
558 $SNIPPET('rift-post-yum')
559 $SNIPPET('rift-post')
560 $SNIPPET('rift_fix_grub')
563 echo "banner RDO test" >> /etc/profile
565 $SNIPPET('kickstart_done')