2013-05-23 04:25:05 +02:00
#!/usr/bin/python
#coding: utf-8 -*-
2014-01-28 17:20:36 +01:00
# (c) 2013, Benno Joy <benno@ansible.com>
2013-11-23 06:37:47 +01:00
# (c) 2013, John Dewey <john@dewey.ws>
2013-05-23 04:25:05 +02:00
#
# This module is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this software. If not, see <http://www.gnu.org/licenses/>.
2014-08-03 05:31:31 +02:00
import operator
2014-08-03 00:31:10 +02:00
import os
2013-05-23 04:25:05 +02:00
try:
from novaclient.v1_1 import client as nova_client
2014-04-11 21:01:14 +02:00
from novaclient.v1_1 import floating_ips
2013-11-23 06:37:47 +01:00
from novaclient import exceptions
2014-04-11 21:01:14 +02:00
from novaclient import utils
2013-05-23 04:25:05 +02:00
import time
except ImportError:
print("failed=True msg='novaclient is required for this module'")
DOCUMENTATION = '''
---
module: nova_compute
2013-11-19 00:55:49 +01:00
version_added: "1.2"
2013-06-14 11:53:43 +02:00
short_description: Create/Delete VMs from OpenStack
2013-05-23 04:25:05 +02:00
description:
- Create or Remove virtual machines from Openstack.
options:
login_username:
description:
- login username to authenticate to keystone
required: true
default: admin
login_password:
description:
- Password of login user
required: true
2013-06-01 17:52:28 +02:00
default: 'yes'
2013-05-23 04:25:05 +02:00
login_tenant_name:
description:
- The tenant name of the login user
required: true
2013-06-01 17:52:28 +02:00
default: 'yes'
2013-05-23 04:25:05 +02:00
auth_url:
description:
- The keystone url for authentication
required: false
default: 'http://127.0.0.1:35357/v2.0/'
region_name:
description:
- Name of the region
required: false
default: None
state:
description:
- Indicate desired state of the resource
choices: ['present', 'absent']
default: present
name:
description:
2013-10-31 11:34:04 +01:00
- Name that has to be given to the instance
2013-05-23 04:25:05 +02:00
required: true
default: None
image_id:
description:
2014-08-03 03:51:27 +02:00
- The id of the base image to boot. Mutually exclusive with image_name
required: true
default: None
image_name:
description:
- The name of the base image to boot. Mutually exclusive with image_id
2013-05-23 04:25:05 +02:00
required: true
default: None
2014-08-03 05:45:35 +02:00
version_added: "1.7"
2014-08-03 05:31:31 +02:00
image_filter:
description:
- Text to use to filter image names, for the case, such as HP, where there are multiple image names matching the common identifying portions. image_filter is a negative match filter - it is text that may not exist in the image name. Defaults to "(deprecated)"
2014-08-03 05:45:35 +02:00
version_added: "1.7"
2013-05-23 04:25:05 +02:00
flavor_id:
description:
2014-08-03 03:51:27 +02:00
- The id of the flavor in which the new VM has to be created. Mutually exclusive with flavor_ram
required: false
default: 1
flavor_ram:
description:
- The minimum amount of ram in MB that the flavor in which the new VM has to be created must have. Mutually exclusive with flavor_id
2013-05-23 04:25:05 +02:00
required: false
default: 1
2014-08-03 05:45:35 +02:00
version_added: "1.7"
2014-08-03 05:31:31 +02:00
flavor_filter:
description:
- Text to use to filter flavor names, for the case, such as Rackspace, where there are multiple flavors that have the same ram count. flavor_filter is a positive match filter - it must exist in the flavor name.
2014-08-03 05:45:35 +02:00
version_added: "1.7"
2013-05-23 04:25:05 +02:00
key_name:
description:
2013-06-14 11:53:43 +02:00
- The key pair name to be used when creating a VM
2013-05-23 04:25:05 +02:00
required: false
default: None
security_groups:
description:
2013-06-14 11:53:43 +02:00
- The name of the security group to which the VM should be added
2013-05-23 04:25:05 +02:00
required: false
default: None
nics:
description:
2013-11-23 06:37:47 +01:00
- A list of network id's to which the VM's interface should be attached
2013-05-23 04:25:05 +02:00
required: false
default: None
2014-08-03 03:26:23 +02:00
auto_floating_ip:
2014-04-11 21:01:14 +02:00
description:
2014-08-03 03:26:23 +02:00
- Should a floating ip be auto created and assigned
required: false
default: 'yes'
2014-08-03 05:45:35 +02:00
version_added: "1.7"
2014-08-03 03:26:23 +02:00
floating_ips:
decription:
- list of valid floating IPs that pre-exist to assign to this node
required: false
default: None
2014-08-03 05:45:35 +02:00
version_added: "1.7"
2014-08-03 03:26:23 +02:00
floating_ip_pools:
description:
- list of floating IP pools from which to choose a floating IP
2014-04-11 21:01:14 +02:00
required: false
default: None
2014-08-03 05:45:35 +02:00
version_added: "1.7"
2014-08-03 00:39:50 +02:00
availability_zone:
description:
- Name of the availability zone
required: false
default: None
2014-08-03 05:45:35 +02:00
version_added: "1.7"
2013-05-23 04:25:05 +02:00
meta:
description:
2013-06-14 11:53:43 +02:00
- A list of key value pairs that should be provided as a metadata to the new VM
2013-05-23 04:25:05 +02:00
required: false
default: None
wait:
description:
2013-06-14 11:53:43 +02:00
- If the module should wait for the VM to be created.
2013-05-23 04:25:05 +02:00
required: false
default: 'yes'
wait_for:
description:
2013-06-14 11:53:43 +02:00
- The amount of time the module should wait for the VM to get into active state
2013-05-23 04:25:05 +02:00
required: false
default: 180
2014-02-08 15:45:03 +01:00
user_data:
description:
- Opaque blob of data which is made available to the instance
required: false
default: None
2014-03-11 19:45:04 +01:00
version_added: "1.6"
2013-06-14 11:53:43 +02:00
requirements: ["novaclient"]
'''
EXAMPLES = '''
# Creates a new VM and attaches to a network and passes metadata to the instance
- nova_compute:
2013-05-23 04:25:05 +02:00
state: present
login_username: admin
login_password: admin
login_tenant_name: admin
name: vm1
image_id: 4f905f38-e52a-43d2-b6ec-754a13ffb529
key_name: ansible_key
wait_for: 200
flavor_id: 4
nics:
- net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723
meta:
hostname: test1
2013-06-14 11:53:43 +02:00
group: uge_master
2014-04-11 21:01:14 +02:00
2014-08-03 00:39:50 +02:00
# Creates a new VM in HP Cloud AE1 region availability zone az2 and automatically assigns a floating IP
2014-04-11 21:01:14 +02:00
- name: launch a nova instance
hosts: localhost
tasks:
- name: launch an instance
nova_compute:
state: present
2014-08-03 05:45:35 +02:00
login_username: username
login_password: Equality7-2521
2014-04-11 21:01:14 +02:00
login_tenant_name: username-project1
name: vm1
auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/
region_name: region-b.geo-1
2014-08-03 00:39:50 +02:00
availability_zone: az2
2014-04-11 21:01:14 +02:00
image_id: 9302692b-b787-4b52-a3a6-daebb79cb498
key_name: test
wait_for: 200
flavor_id: 101
security_groups: default
2014-08-03 05:45:35 +02:00
auto_floating_ip: yes
2014-04-11 21:01:14 +02:00
2014-08-03 05:45:35 +02:00
# Creates a new VM in HP Cloud AE1 region availability zone az2 and assigns a pre-known floating IP
- name: launch a nova instance
hosts: localhost
tasks:
- name: launch an instance
nova_compute:
state: present
login_username: username
login_password: Equality7-2521
login_tenant_name: username-project1
name: vm1
auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/
region_name: region-b.geo-1
availability_zone: az2
image_id: 9302692b-b787-4b52-a3a6-daebb79cb498
key_name: test
wait_for: 200
flavor_id: 101
floating-ips:
- 12.34.56.79
# Creates a new VM with 4G of RAM on Ubuntu Trusty, ignoring deprecated images
- name: launch a nova instance
hosts: localhost
tasks:
- name: launch an instance
nova_compute:
name: vm1
state: present
login_username: username
login_password: Equality7-2521
login_tenant_name: username-project1
auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/
region_name: region-b.geo-1
image_name: Ubuntu Server 14.04
image_filter: deprecated
flavor_ram: 4096
2014-04-11 21:01:14 +02:00
2014-08-03 05:45:35 +02:00
# Creates a new VM with 4G of RAM on Ubuntu Trusty on a Rackspace Performance node in DFW
- name: launch a nova instance
hosts: localhost
tasks:
- name: launch an instance
nova_compute:
name: vm1
state: present
login_username: username
login_password: Equality7-2521
login_tenant_name: username-project1
auth_url: https://identity.api.rackspacecloud.com/v2.0/
region_name: DFW
image_name: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)
flavor_ram: 4096
flavor_filter: Performance
2013-05-23 04:25:05 +02:00
'''
2013-06-14 11:53:43 +02:00
2014-04-11 21:01:14 +02:00
2013-05-23 04:25:05 +02:00
def _delete_server(module, nova):
name = None
2013-10-10 08:14:30 +02:00
server_list = None
2013-05-23 04:25:05 +02:00
try:
2013-10-10 08:14:30 +02:00
server_list = nova.servers.list(True, {'name': module.params['name']})
if server_list:
server = [x for x in server_list if x.name == module.params['name']]
nova.servers.delete(server.pop())
2014-02-02 18:33:27 +01:00
except Exception, e:
2013-05-25 05:34:48 +02:00
module.fail_json( msg = "Error in deleting vm: %s" % e.message)
2013-05-23 04:25:05 +02:00
if module.params['wait'] == 'no':
2013-05-25 05:34:48 +02:00
module.exit_json(changed = True, result = "deleted")
2013-09-08 23:36:37 +02:00
expire = time.time() + int(module.params['wait_for'])
2013-05-23 04:25:05 +02:00
while time.time() < expire:
2013-05-31 14:09:00 +02:00
name = nova.servers.list(True, {'name': module.params['name']})
2013-05-25 05:34:48 +02:00
if not name:
module.exit_json(changed = True, result = "deleted")
time.sleep(5)
2013-05-23 04:25:05 +02:00
module.fail_json(msg = "Timed out waiting for server to get deleted, please check manually")
2014-08-03 03:26:23 +02:00
def _add_floating_ip_from_pool(module, nova, server):
# instantiate FloatingIPManager object
floating_ip_obj = floating_ips.FloatingIPManager(nova)
2014-08-03 00:05:42 +02:00
# empty dict and list
usable_floating_ips = {}
pools = []
# user specified
2014-08-03 03:26:23 +02:00
pools = module.params['floating_ip_pools']
2014-08-03 00:05:42 +02:00
# get the list of all floating IPs. Mileage may
# vary according to Nova Compute configuration
# per cloud provider
all_floating_ips = floating_ip_obj.list()
# iterate through all pools of IP address. Empty
# string means all and is the default value
for pool in pools:
# temporary list per pool
pool_ips = []
# loop through all floating IPs
for f_ip in all_floating_ips:
# if not reserved and the correct pool, add
if f_ip.instance_id is None and (f_ip.pool == pool):
pool_ips.append(f_ip.ip)
# only need one
break
# if the list is empty, add for this pool
if not pool_ips:
try:
new_ip = nova.floating_ips.create(pool)
except Exception, e:
module.fail_json(msg = "Unable to create floating ip")
pool_ips.append(new_ip.ip)
# Add to the main list
usable_floating_ips[pool] = pool_ips
# finally, add ip(s) to instance for each pool
for pool in usable_floating_ips:
for ip in usable_floating_ips[pool]:
try:
server.add_floating_ip(ip)
# We only need to assign one ip - but there is an inherent
# race condition and some other cloud operation may have
# stolen an available floating ip
break
except Exception, e:
module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message))
2014-08-03 03:26:23 +02:00
def _add_floating_ip_list(module, server):
# add ip(s) to instance
for ip in module.params['floating_ips']:
try:
server.add_floating_ip(ip)
except Exception, e:
module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message))
2014-08-03 00:05:42 +02:00
2014-08-03 03:26:23 +02:00
def _add_auto_floating_ip(module, nova, server):
2014-08-03 00:05:42 +02:00
2014-08-03 03:26:23 +02:00
try:
new_ip = nova.floating_ips.create()
except Exception as e:
module.fail_json(msg = "Unable to create floating ip: %s" % (e.message))
2014-08-03 00:05:42 +02:00
2014-08-03 03:26:23 +02:00
try:
server.add_floating_ip(new_ip)
except Exception as e:
# Clean up - we auto-created this ip, and it's not attached
# to the server, so the cloud will not know what to do with it
server.floating_ips.delete(new_ip)
module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message))
2014-08-03 00:05:42 +02:00
def _add_floating_ip(module, nova, server):
2014-08-03 03:26:23 +02:00
if module.params['floating_ip_pools']:
_add_floating_ip_from_pool(module, nova, server)
elif module.params['floating_ips']:
_add_floating_ip_list(module, server)
elif module.params['auto_floating_ip']:
_add_auto_floating_ip(module, nova, server)
else:
return server
2014-08-03 00:05:42 +02:00
# this may look redundant, but if there is now a
# floating IP, then it needs to be obtained from
# a recent server object if the above code path exec'd
try:
server = nova.servers.get(server.id)
except Exception, e:
module.fail_json(msg = "Error in getting info from instance: %s " % e.message)
return server
2014-08-03 00:42:12 +02:00
def _get_ips(addresses, ext_tag, key_name):
ret = []
for (k, v) in addresses.iteritems():
if k == key_name:
ret.extend([addrs['addr'] for addrs in v])
else:
for interface_spec in v:
if 'OS-EXT-IPS:type' in interface_spec and interface_spec['OS-EXT-IPS:type'] == ext_tag:
ret.append(interface_spec['addr'])
return ret
2014-08-03 03:51:27 +02:00
def _get_image_id(module, nova):
if module.params['image_name']:
2014-08-03 05:31:31 +02:00
for image in nova.images.list():
if (module.params['image_name'] in img.name and (
not module.params['image_filter']
or module.params['image_filter'] not in img.name)):
return image.id
module.fail_json(msg = "Error finding image id from name(%s)" % module.params['image_name'])
2014-08-03 03:51:27 +02:00
return module.params['image_id']
def _get_flavor_id(module, nova):
if module.params['flavor_ram']:
2014-08-03 05:31:31 +02:00
for flavor in sorted(nova.flavors.list(), key=operator.attrgetter('ram')):
if (flavor.ram >= module.params['flavor_ram'] and
(not module.params['flavor_filter'] or module.params['flavor_filter'] in flavor.name)):
return flavor.id
2014-08-03 03:51:27 +02:00
module.fail_json(msg = "Error finding flavor with %sMB of RAM" % module.params['flavor_ram'])
return module.params['flavor_id']
2013-05-23 04:25:05 +02:00
def _create_server(module, nova):
2014-08-03 03:51:27 +02:00
image_id = _get_image_id(module, nova)
flavor_id = _get_flavor_id(module, nova)
bootargs = [module.params['name'], image_id, flavor_id]
2013-05-23 04:25:05 +02:00
bootkwargs = {
2013-05-25 05:34:48 +02:00
'nics' : module.params['nics'],
'meta' : module.params['meta'],
'security_groups': module.params['security_groups'].split(','),
2014-02-08 15:45:03 +01:00
#userdata is unhyphenated in novaclient, but hyphenated here for consistency with the ec2 module:
'userdata': module.params['user_data'],
2013-05-23 04:25:05 +02:00
}
2014-08-03 00:15:18 +02:00
2014-08-03 00:39:50 +02:00
for optional_param in ('region_name', 'key_name', 'availability_zone'):
2014-08-03 00:15:18 +02:00
if module.params[optional_param]:
bootkwargs[optional_param] = module.params[optional_param]
2013-05-23 04:25:05 +02:00
try:
2013-05-25 05:46:23 +02:00
server = nova.servers.create(*bootargs, **bootkwargs)
2013-05-25 05:34:48 +02:00
server = nova.servers.get(server.id)
2014-02-02 18:33:27 +01:00
except Exception, e:
2013-05-25 05:34:48 +02:00
module.fail_json( msg = "Error in creating instance: %s " % e.message)
2013-05-23 04:25:05 +02:00
if module.params['wait'] == 'yes':
2013-09-08 23:36:37 +02:00
expire = time.time() + int(module.params['wait_for'])
2013-05-25 05:34:48 +02:00
while time.time() < expire:
try:
2013-05-23 04:25:05 +02:00
server = nova.servers.get(server.id)
2014-02-02 18:33:27 +01:00
except Exception, e:
2014-08-03 00:05:42 +02:00
module.fail_json( msg = "Error in getting info from instance: %s" % e.message)
2013-05-25 05:34:48 +02:00
if server.status == 'ACTIVE':
2014-08-03 03:26:23 +02:00
server = _add_floating_ip(module, nova, server)
2014-04-11 21:01:14 +02:00
2014-08-03 00:42:12 +02:00
private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private')
public = _get_ips(getattr(server, 'addresses'), 'floating', 'public')
2014-04-11 21:01:14 +02:00
# now exit with info
2013-05-25 05:34:48 +02:00
module.exit_json(changed = True, id = server.id, private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info)
2014-04-11 21:01:14 +02:00
2013-05-25 05:34:48 +02:00
if server.status == 'ERROR':
module.fail_json(msg = "Error in creating the server, please check logs")
time.sleep(2)
2013-11-23 06:37:47 +01:00
2013-05-25 05:34:48 +02:00
module.fail_json(msg = "Timeout waiting for the server to come up.. Please check manually")
2013-05-23 04:25:05 +02:00
if server.status == 'ERROR':
2013-05-25 05:34:48 +02:00
module.fail_json(msg = "Error in creating the server.. Please check manually")
2014-08-03 00:42:12 +02:00
private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private')
public = _get_ips(getattr(server, 'addresses'), 'floating', 'public')
2014-04-11 21:01:14 +02:00
2013-05-23 04:25:05 +02:00
module.exit_json(changed = True, id = info['id'], private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info)
2013-11-23 06:37:47 +01:00
2013-05-23 04:25:05 +02:00
def _get_server_state(module, nova):
server = None
try:
2013-05-31 14:09:00 +02:00
servers = nova.servers.list(True, {'name': module.params['name']})
2013-05-25 05:34:48 +02:00
if servers:
2014-03-01 03:05:52 +01:00
# the {'name': module.params['name']} will also return servers
# with names that partially match the server name, so we have to
# strictly filter here
servers = [x for x in servers if x.name == module.params['name']]
2014-03-03 19:55:28 +01:00
if servers:
server = servers[0]
2014-02-02 18:33:27 +01:00
except Exception, e:
2013-05-25 05:34:48 +02:00
module.fail_json(msg = "Error in getting the server list: %s" % e.message)
2013-05-23 04:25:05 +02:00
if server and module.params['state'] == 'present':
2013-05-25 05:34:48 +02:00
if server.status != 'ACTIVE':
module.fail_json( msg="The VM is available but not Active. state:" + server.status)
2014-08-03 00:42:12 +02:00
private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private')
public = _get_ips(getattr(server, 'addresses'), 'floating', 'public')
2013-11-23 06:37:47 +01:00
module.exit_json(changed = False, id = server.id, public_ip = ''.join(public), private_ip = ''.join(private), info = server._info)
2013-05-23 04:25:05 +02:00
if server and module.params['state'] == 'absent':
2013-05-25 05:34:48 +02:00
return True
2013-05-23 04:25:05 +02:00
if module.params['state'] == 'absent':
2013-05-25 05:34:48 +02:00
module.exit_json(changed = False, result = "not present")
2013-05-23 04:25:05 +02:00
return True
2013-11-23 06:37:47 +01:00
2013-05-23 04:25:05 +02:00
def main():
2014-08-03 02:12:24 +02:00
argument_spec = openstack_argument_spec()
argument_spec.update(dict(
2013-05-25 05:34:48 +02:00
name = dict(required=True),
2013-11-23 06:37:47 +01:00
image_id = dict(default=None),
2014-08-03 03:51:27 +02:00
image_name = dict(default=None),
2014-08-03 05:31:31 +02:00
image_filter = dict(default='(deprecated)'),
2013-05-25 05:34:48 +02:00
flavor_id = dict(default=1),
2014-08-03 03:51:27 +02:00
flavor_ram = dict(default=None, type='int'),
2014-08-03 05:31:31 +02:00
flavor_filter = dict(default=None),
2013-05-25 05:34:48 +02:00
key_name = dict(default=None),
security_groups = dict(default='default'),
nics = dict(default=None),
meta = dict(default=None),
wait = dict(default='yes', choices=['yes', 'no']),
2013-09-08 23:38:48 +02:00
wait_for = dict(default=180),
2014-02-08 15:45:03 +01:00
state = dict(default='present', choices=['absent', 'present']),
2014-04-11 21:01:14 +02:00
user_data = dict(default=None),
2014-08-03 03:26:23 +02:00
auto_floating_ip = dict(default=False, type='bool'),
floating_ips = dict(default=None),
floating_ip_pools = dict(default=None),
2014-08-03 02:12:24 +02:00
))
2014-08-03 03:26:23 +02:00
module = AnsibleModule(
argument_spec=argument_spec,
mutually_exclusive=[
['auto_floating_ip','floating_ips'],
['auto_floating_ip','floating_ip_pools'],
['floating_ips','floating_ip_pools'],
2014-08-03 03:51:27 +02:00
['image_id','image_name'],
['flavor_id','flavor_ram'],
2014-08-03 03:26:23 +02:00
],
)
2013-11-23 06:37:47 +01:00
nova = nova_client.Client(module.params['login_username'],
module.params['login_password'],
module.params['login_tenant_name'],
module.params['auth_url'],
2014-03-20 14:07:49 +01:00
region_name=module.params['region_name'],
2013-11-23 06:37:47 +01:00
service_type='compute')
2013-05-23 04:25:05 +02:00
try:
2013-11-23 06:37:47 +01:00
nova.authenticate()
2014-03-04 17:30:15 +01:00
except exceptions.Unauthorized, e:
2013-11-23 06:37:47 +01:00
module.fail_json(msg = "Invalid OpenStack Nova credentials.: %s" % e.message)
2014-03-04 17:30:15 +01:00
except exceptions.AuthorizationFailure, e:
2013-11-23 06:37:47 +01:00
module.fail_json(msg = "Unable to authorize user: %s" % e.message)
2013-05-23 04:25:05 +02:00
if module.params['state'] == 'present':
2014-08-03 03:51:27 +02:00
if not module.params['image_id'] and not module.params['image_name']:
module.fail_json( msg = "Parameter 'image_id' or `image_name` is required if state == 'present'")
2013-08-17 17:56:19 +02:00
else:
_get_server_state(module, nova)
_create_server(module, nova)
2013-05-23 04:25:05 +02:00
if module.params['state'] == 'absent':
2013-05-25 05:34:48 +02:00
_get_server_state(module, nova)
_delete_server(module, nova)
2013-11-23 06:37:47 +01:00
2014-08-03 05:31:31 +02:00
# this is magic, see lib/ansible/module_common.py
2013-12-02 21:11:23 +01:00
from ansible.module_utils.basic import *
2014-08-03 02:12:24 +02:00
from ansible.module_utils.openstack import *
2013-05-23 04:25:05 +02:00
main()