Thursday, November 12, 2009

Synchronization of files with SSH and Python

Specification of the problem

I have to maintain a system that needs to be updated from time to time. Update files are uploaded for me to a directory on a
host where I have an ssh account. Let us call this host repomaster.

Rsync, ftp, http etc. are not available.


Repomaster can be accessed from one host only, let us call it repoproxy.

The files are needed on several hosts, on which the application is installed. For the sake of simplicity let us assume that
we have two such hosts: appnode1 and appnode2.

All the involved hosts are some kind of Unix/Linux, but are not the same.

The files are big, and can be updated at any moment, usually Friday evening, when everybody already has gone home. When the new
files are uploaded, they overwrite the old ones. There is nothing besides the files themselves (their modification
time, size and contents) indicating that files on the repomaster changed.


When I come to work on Monday morning, I find a notification email that the application needs to be updated. All the team
members also get the very same notification email and are extremely anxious to have all the application nodes updated
as soon as possible.

Doing this manually is a very unpleasant work - one has to first copy the files from repomaster to repoproxy, and then from
repoproxy to all the application nodes. The network performance is poor, sessions oftentimes get broken, files get corrupted. One has to
change between many screen sessions on several hosts, verify the checksums of files etc. It is an unpleasant and error prone process
that takes several hours to complete manually.

What I wanted was that all this happens automatically during the night, even on weekends or hollidays, so that on Monday morning I only
have to check the logs to verify that everything completed successfully. And, if something did not work, I only have to run one
single command after I correct the error condition to have the job proceed from where it failed.


Solution design

Since neither rsync nor wget (with http or ftp server) are available, I decided to use Python with standard SSH and SCP.

SSH is used by calling comand line directly, not through Paramiko or Twisted libraries. Unix path separator is hardcoded, because
this script is a quick hack to save me manual work until a better production script is produced. (“Better” means something more
sophisticated, transferring files in smaller chunks and more robust - recovering quicker from network failures and re-transferring
only the broken chunks, not whole files).


In this article I share with you only the script that copies the files from parent file repository to the child repository. Code that
deletes the files that were deleted on the parent is not published here to make this article shorter.

The general idea is that when we run this command:

sshsync --sync repoproxy --sync appnode1,appnode2

our script will fetch the configuration of the repoproxy and its parent, repomaster, find all the
in the repomaster, copy the ones that were updated in the master repo, and then move to the next step.

The next step in our example would be to synchronize appnode1 and appnode2 in the same way as we
did with repoproxy in the previous step. Since these nodes were specified as one step, they are done
in parallel, to save time.


Again, to simplify this article, I removed the code for running the synchronization in parallel. The
code that I am sharing with you does not support this, it only supports sequential updates:

sshsync --sync repoproxy --sync appnode1 --sync appnode2

All the needed information - usernames, IPs, remote paths etc. are kept in the configuration file.

The script is simplistic, it uses direct command executed on remote machines with ssh - for this
to be possible the passwordless ssh logins have to be enabled. No /etc/motd or /etc/issue banners
can be displayed on login.

This script can be run on one of the hosts or on separate machine - this does not matter.

Since the target systems run different operating systems and have different tools installed,
the way we get a list of files, calculate a file digest or transfer a file is specified
separately for each host in the configuration file.

Files are transferred if the digests in the current repository and its parent repository do
not match.

The script writes to the log file. Log file is rotated with standard OS tooling (logrotate), this
script does not implement log rotation.


The script is run from cron. Only one instance can be run on a given time.

Solution implementation

Configuration file

Configuration file has a separate section for each repository. Each section contains
two blocks: information needed to find the repository (host, user, repository path) and
its parent (reference to parent configuration block) and the templates for the three
commands that will be used to process information in the repository:

  1. Command to list repository contents
  2. Command to calculate the digest of the file in the repository
  3. Command to transfer a file from the parent repository
#===================================================================
#         FILE:  /etc/sshsync/sshsync.conf
#  DESCRIPTION:  SSH synchronization tooling config
#===================================================================

[repomaster]

user = repoowner
password = ******
host = 192.168.0.1
path = /home/repoowner/apprepo

listcmd = find ${REPOPATH} -type f
digestcmd = if [ -f ${FILEPATH} ]; then sha1sum ${FILEPATH} | cut -d' ' -f1; else echo \'NONE\'; fi


[repoproxy]

user = proxyrunner
password = ******
host = 192.168.1.1
path = /dat/repoproxy
parent = repomaster

listcmd = find ${REPOPATH} -type f
digestcmd = if [ -f ${FILEPATH} ]; then sha1sum ${FILEPATH} | cut -d' ' -f1; else echo \'NONE\'; fi
synccmd = scp -p -i /home/proxyrunner/.ssh/id_dsa ${REMOTEUSER}@${REMOTEHOST}:${REMOTEDIR}/${FILE} ${LOCALDIR}/${FILE}

[appnode1]

user = john
password = ******
host = 192.168.2.1
path = /apprepo/latest
parent = repoproxy

listcmd = find ${REPOPATH} -type f
digestcmd = if [ -f ${FILEPATH} ]; then digest -a sha1 ${FILEPATH}; else echo \'NONE\'; fi
synccmd = scp -p -i /home/john/.ssh/id_dsa ${REMOTEUSER}@${REMOTEHOST}:${REMOTEDIR}/${FILE} ${LOCALDIR}/${FILE}

[appnode2]

user = sheila
password = ******
host = 192.168.2.2
path = /home/sheila/apprepo
parent = repoproxy

listcmd = find ${REPOPATH} -type f
digestcmd = if [ -f ${FILEPATH} ]; then digest -a sha1 ${FILEPATH}; else echo \'NONE\'; fi
synccmd = scp -p -i /home/sheila/.ssh/id_dsa ${REMOTEUSER}@${REMOTEHOST}:${REMOTEDIR}/${FILE} ${LOCALDIR}/${FILE}

As you can see I did not use the tools that Python’s ConfigParser module gives me to resolve variables defined
in other parts of the file. I use the variables in a shell-like notation and replace them in the script. In my
opinion this is easier to maintain, as I can update the parent-child relationship between repositories much esier.

Patterns for listcmd, digestcmd and synccmd are bash oneliners, but nothing prevents them to be more
sophisticated, separate scripts. They can be different for each repository - as you can see, I sometimes
use digest command to calculate SHA1 of a file, and sometimes I use sha1sum for the same purpose. I do it because
some hosts do not have sha1sum installed. The important thing is, that these commands return exactly the same strings.


Control script

Let me present the control script in chunks.

#!/bin/env python
"""
File repository synchronization with SSH.
Compares digests of files, and transfers them if necessary.
Requires passwordless ssh logins, no output on ssh login, availability of
list, digest and transfer commands on all respective repo servers.

"""

import os
import sys
import getopt
import ConfigParser

import logging, logging.config
import commands

This is a standard shebang to allow calling the script as a command plus docstring and necessary imports.
Modules os and sys are needed for operating system related stuff, getopt for option parsing,

ConfigParser for handling our configuration file, logging for logging support and commands for
calling command line programs and collecting their return code and output.

class UsageException(Exception):

  """
  Exception thrown when parameters are wrong.
  """
  def __init__(self, msg):

      self.msg = msg

class InternalErrorException(Exception):

  """
  Exception thrown when internal application error occurs.
  """
  def __init__(self, msg):

      self.msg = msg

class ProcessingErrorException(Exception):

  """
  Exception thrown when application encountered an external error.
  """
  def __init__(self, msg):

      self.msg = msg

Well, here I deviate a bit from my personal standards. Usually I define one parent exception class and
make all the exceptions that a given application can throw extend it. That way it is easy for any code
importing our module to catch all our exceptions.

In this case I am being lazy, I just define three separate exceptions: one for bad usage, one for
internal bugs, and one for outside conditions that may make the application to fail.

config = None

log = None
cmd = commands.getstatusoutput

scriptconfig = '/etc/sshsync/sshsync.conf'

logconfig = '/etc/sshsync/logging.conf'
pidfile = '/var/run/sshsync.pid'

Globals. Config and log are to be instantiated later in the code, cmd is a mere shortcut, the rest
are the locations of files used by the script. I do not explain logconfig here, as this is plain, standard
configuration file for Python logging.


def print_usage():
  """
  Print usage information.
  """
  print >>sys.stdout, """\

  Usage:
      sshsync --option=<value> ...
  Options:
      -h / --help
          Print this help message and exit

      -s / --sync <repository>
          Synchronize <repository> with it's parent
  """
  return 0

This is just a funtion called to print a standard help message. Notice that it returns, not exits.

def lock(lockfile):
  """ """
  rc = True

  if os.path.exists(lockfile):
      rc = False

      log.debug('Can not create lock file. Lock file already exists: %s' % lockfile)
  else:

      log.debug('Creating lock file: %s' % lockfile)
      try:

          f = open(lockfile, 'w')
          f.write(str(os.getpid()))

          f.close()
          log.debug('Lock file created: %s' % lockfile)

      except IOError, error:
          rc = False
          log.error('Can not create lock file. I/O error: %s' % error)

  return rc

def unlock(lockfile):
  """ """

  rc = True
  if not os.path.exists(lockfile):

      rc = False
      log.error('Unlock called, but lock file does not exist: %s' % lockfile)

  else:
      log.debug('Removing lock file: %s' % lockfile)

      try:
          os.unlink(lockfile)
          log.debug('Lock file deleted: %s' % lockfile)

      except IOError, error:
          rc = False
          log.error('Can not delete lock file. I/O error: %s' % error)

  return rc

Our script should have only one, single instance at any given time. I use a PID file to
find out whether there is any other instance running, or not.

def runrmtcmd(host, user, password, rmtcmd):

  """ """
  status, out = cmd('ssh -l %s %s "%s"' % (user, host, rmtcmd))

  if status != 0:
      raise ProcessingErrorException('Can not run command "%s" on host "%s": %s' % (rmtcmd, host, out))

  return out

A tool to run a command on the remote system. Password is ignored. I keep it just in case if later I
want to be more trendy and use Twisted libraries to properly login with SSH.

This function relies on passwordless logins enabled for SSH. Notice that non-zero return code triggers
throwing of exception.

As you can see, I am pretty lazy with writing docstrings.

def getrmtfiles(repository):

  """ """
  files = []
  log.debug('Getting list of files in the repository: %s' % repository)

  repo_host = config.get(repository, 'host')
  repo_user = config.get(repository, 'user')

  repo_password = config.get(repository, 'password')
  repo_path = os.path.normpath(config.get(repository, 'path'))

  listcmd = config.get(repository, 'listcmd')
  listcmd = listcmd.replace('${REPOPATH}', repo_path)

  out = runrmtcmd(repo_host, repo_user, repo_password, listcmd)

  prefix = len(repo_path) + 1
  lines = out.splitlines()

  for line in lines:
      f = line[prefix:]

      files.append(f)
      log.debug('Repository %s has file: %s' % (repository, f))

  return files

This is the function that we call to get a list of files in the repository.

Config is a global that must be initialized before we call this function. Again, this is being
lazy on my part, I would not write such a thing in a library class. Yes, I agree, it would be
cleaner to have a Repository object instead of a bunch of functions and global variables...


Config is wise enough to fetch strings from our configuration file. Read ConfigParser documentation
to learn more about it.

I am cutting off the repository path from the file name, because the repositories can be placed in different
places on different machines. I use substring instead of os.path functions, because I do not want to deal
with leading dots in file paths.

def getrmtdigest(repository, fpath):

  """ """
  log.debug('Getting digest from repository %s for file: %s' % (repository, fpath))

  repo_host = config.get(repository, 'host')
  repo_user = config.get(repository, 'user')

  repo_password = config.get(repository, 'password')
  repo_path = os.path.normpath(config.get(repository, 'path'))

  digestcmd = config.get(repository, 'digestcmd')
  digestcmd = digestcmd.replace('${FILEPATH}', '%s/%s' % (repo_path, fpath))

  digest = runrmtcmd(repo_host, repo_user, repo_password, digestcmd)

  log.debug('Digest from repository %s for file %s : %s' % (repository, fpath, digest))

  return digest

This is the function that gets the digest of a file in a repository. As you can see, I am
repeating myself to fetch the user, password and host from the configuration. If this code were to
be used longer, I would make a repository object and fetch all the configuration only once in __init__
function.

I leave this improvement as an exercise to the visitors of my blog.

def syncrmtfile(repository, fpath):

  """ """

  repo_host = config.get(repository, 'host')

  repo_user = config.get(repository, 'user')
  repo_password = config.get(repository, 'password')

  repo_path = os.path.normpath(config.get(repository, 'path'))

  parent = config.get(repository, 'parent')
  parent_host = config.get(parent, 'host')

  parent_user = config.get(parent, 'user')
  parent_password = config.get(parent, 'password')

  parent_path = os.path.normpath(config.get(parent, 'path'))

  rmtcmd = config.get(repository, 'synccmd')
  rmtcmd = rmtcmd.replace('${FILE}', fpath)

  rmtcmd = rmtcmd.replace('${REMOTEHOST}', parent_host)
  rmtcmd = rmtcmd.replace('${REMOTEUSER}', parent_user)

  rmtcmd = rmtcmd.replace('${REMOTEPASSWORD}', parent_password)
  rmtcmd = rmtcmd.replace('${REMOTEDIR}', parent_path)

  rmtcmd = rmtcmd.replace('${LOCALHOST}', repo_host)
  rmtcmd = rmtcmd.replace('${LOCALUSER}', repo_user)

  rmtcmd = rmtcmd.replace('${LOCALPASSWORD}', repo_password)
  rmtcmd = rmtcmd.replace('${LOCALDIR}', repo_path)

  log.info('Syncing file in repository %s: %s' % (repository, fpath))

  cmdout = runrmtcmd(repo_host, repo_user, repo_password, rmtcmd )

  for line in cmdout.splitlines():
      log.debug("Host %s: %s" % (repo_host, line))

Code used to copy a single file from the parent repository to the current one.

Again, DRY principle violated. Sorry about that.

def syncrepo(repository):
  """ """
  log.info('Syncing repository: %s' % repository)

  log.debug('This script assumes passwordless logins with ssh!')

  parent = config.get(repository, 'parent')

  parent_files = getrmtfiles(parent)

  for f in parent_files:

      pdigest = getrmtdigest(parent, f)
      rdigest = getrmtdigest(repository, f)

      if pdigest == rdigest:
          log.debug('File %s in %s and %s have the same digest (%s). Synchronization is not needed.' % (f, parent, repository, rdigest))

      else:
          syncrmtfile(repository, f)
          rdigest = getrmtdigest(repository, f)

          if pdigest == rdigest:
              log.info('File %s has been successfully copied from %s to %s. File digest is: %s' % (f, parent, repository, rdigest))

          else:
              log.error('Transfer of file %s from %s to %s failed. Digests mismatch (%s != %s)' % (f, parent, repository, pdigest, rdigest))

A function capable of syncing all the files in a single repository. It gets a list of files and for each file
compares its digest in the current and parent repositories. If the digests do not match, the file is being copied from the parrent.

That is wasting the bandwidth, but at least easy to write.

def checkconfig(repository):
  """ """
  rc = True

  parent = None
  if config.has_section(repository):

      if not config.has_option(repository, 'host') or not config.get(repository, 'host'):

          rc = False
          log.error('Missing host for repository %s' % repository)

      if not config.has_option(repository, 'user') or not config.get(repository, 'user'):

          rc = False
          log.error('Missing user for repository %s' % repository)

      if not config.has_option(repository, 'password') or not config.get(repository, 'password'):

          rc = False
          log.error('Missing password for repository %s' % repository)

      if not config.has_option(repository, 'path') or not config.get(repository, 'path'):

          rc = False
          log.error('Missing path for repository %s' % repository)

      if not config.has_option(repository, 'digestcmd') or not config.get(repository, 'digestcmd'):

          rc = False
          log.error('Missing digestcmd for repository %s' % repository)

      if not config.has_option(repository, 'synccmd') or not config.get(repository, 'synccmd'):

          rc = False
          log.error('Missing synccmd for repository %s' % repository)

      if config.has_option(repository, 'parent'):
          parent = config.get(repository, 'parent')

          if not parent:
              rc = False
              log.error('Parent repository name missing for %s' % repository)

          else:
              if not config.has_option(parent, 'host') or not config.get(parent, 'host'):

                  rc = False
                  log.error('Missing host for repository %s' % parent)

              if not config.has_option(parent, 'user') or not config.get(parent, 'user'):

                  rc = False
                  log.error('Missing user for repository %s' % parent)

              if not config.has_option(parent, 'password') or not config.get(parent, 'password'):

                  rc = False
                  log.error('Missing password for repository %s' % parent)

              if not config.has_option(parent, 'path') or not config.get(parent, 'path'):

                  rc = False
                  log.error('Missing path for repository %s' % parent)

              if not config.has_option(parent, 'listcmd') or not config.get(parent, 'listcmd'):

                  rc = False
                  log.error('Missing listcmd for repository %s' % parent)

              if not config.has_option(parent, 'digestcmd') or not config.get(parent, 'digestcmd'):

                  rc = False
                  log.error('Missing digestcmd for repository %s' % parent)

      else:
          rc = False
          log.error('Repository %s does not have parent repository.' % repository)

  else:
      rc = False
      log.error('Missing configuration for repository: %s' % repository)

  return rc

Before we start doing the job, we want to know if our configuration file can be parsed and contains
all the information that we will need. This function is supposed to take care of that.

I am checking if all the keys are present and if they are not empty.

def main(argv=None):

  """ """

  if argv is None:
      argv = sys.argv[1:]

  global config
  global log

  config = ConfigParser.ConfigParser()

  config.read(scriptconfig)

  logging.config.fileConfig(logconfig)

  log = logging.getLogger('mbrepo')

  opts, args = getopt.getopt(argv, 'hs:', ["help","sync="])

  syncqueue = []

  for opt, arg in opts:

      if opt in ('-h', '--help'):
          return print_usage()

      elif opt in ('-s', '--sync'):
          if checkconfig(arg):

              syncqueue.append(arg)
              log.debug('Scheduled repo for syncing: %s' % arg)

          else:
              raise ProcessingErrorException("Missing configuration to sync repository: %s" % arg)

      else:
          raise UsageException('Unknown option: ' + opt)

  if lock(pidfile):
      try:
          log.info('Begin repository synchronization. Syncqueue: %s' % ', '.join(syncqueue))

          for repo in syncqueue:
              syncrepo(repo)
      finally:

          unlock(pidfile)
  else:
      log.warning('Terminating - could not create run lock.')

Well, pretty standard main function, doing all the job a script is supposed to do.

Get the arguments cutting off the script name, initialize configuration and logging machinery, parse
the options, create lock, synchronize everything, remove the lock. Allow all exceptions to
percolate (let the ones who call us decide what to do with exceptions), only make sure that the lock
file is not left behind - otherwise cron would not be able to start the script again until the
operator removes the lock manually.

if __name__ == "__main__":

  rc = 0

  try:
      rc = main(sys.argv[1:])

  except getopt.error, error:
      logging.error('Bad call: %s' % error.msg)

      print >>sys.stderr, error.msg
      print >>sys.stderr, "For help use --help"

      rc = 1

  except UsageException, err:

      logging.error('Bad call: %s' % err.msg)
      print >>sys.stderr, err.msg

      print >>sys.stderr, "For help use --help"
      rc = 2

  except ConfigParser.ParsingError, err:
      print >>sys.stderr, err

      print >>sys.stderr, "Please correct your configuration file!"
      rc = 3

  except ConfigParser.NoSectionError, error:
      print >>sys.stderr, error

      print >>sys.stderr, "Please extend your configuration file!"
      rc = 4

  except ConfigParser.NoOptionError, error:
      print >>sys.stderr, error

      print >>sys.stderr, "Please correct your configuration file!"
      rc = 3

  except InternalErrorException, err:
      logging.critical('Internal error: %s' % err.msg)

      print >>sys.stderr, 'Internal application error occured: %s' % err.msg

      rc = 5

  except ProcessingErrorException, err:

      logging.critical('Processing error: %s' % err.msg)
      print >>sys.stderr, 'Terminating on processing error: %s' % err.msg

      rc = 6

  except:
      raise

  sys.exit(rc)

This is the fragment that calls the main function. We call it only if run
directly from command line, catch the exceptions and convert them to error
messages and error return codes.

Finishing steps

In my case I package the script as RPM, add logrotate configuration and add it to cron.


Wednesday, October 14, 2009

Indonesia 2009 - prices

  • My first blog, my first post
  • Indonesia 2009 - prices

    • Introductory remarks
    • Sumatra
    • Java
    • Bali


My first blog, my first post


I got inspired by a discussion on LinkedIn about blogging. Until now my opinion was that blogging would not make much sense for me. I do not do anything so important that others would like to monitor its progress.

But after thinking a bit, I decided that it might not be a bad idea. Maybe some of my friends would like to know what I am doing, and I may want to come back later to my own posts. I might also get some suggestions from strangers about how to do things better. Writing on a blog can help me to improve my English and general writing skills.

I hope that this blog will be a learning experience for me.

Indonesia 2009 - prices


I have just returned from a trip to Indonesia. This was my second visit to this beautiful country. So far I never used Lonely Planet during any of my trips, so I always need to spend a few days on research to find out things like how to get from the airport to the city, what prices should I expect, where to search for budget accomodation etc.

During the trip I made noted the prices of basic things, like food, accomodation and transport especially for this blog post.

Introductory remarks


Indonesia is a vast and beautiful country. It is inexpensive place to travel. People are very friendly and always try to make life easier for the tourist. Food is OK, nature is amazing, local culture is rich and fascinating.

Even though it is not all roses and with time you start seeing negative aspects of life in Indonesia, even though tragic natural disasters happen here more often that elsewhere, in my opinion it is a great destination for a holiday trip.

Travelling is very, very easy in Asia, and Indonesia is no exception. There are plenty of cheap flights. I did not use neither my debit nor my credit card in Indonesia, but my friend did, and he did not have any problems with that. ATMs were easy to find everywhere.

I brought cash, and I did have some problems. If you bring US dollars, try to bring banknotes with serial number starting with letter D or “higher”. Many places do not want to accept banknotes with serial number starting with “A”, “B” or “C”. Others will accept them, but give you a lower exchange rates. The bank where I was changing money, charged me with a commision for accepting these banknotes. Euros were not accepted in places where I was changing money.

Getting to Indonesia from Europe is easy - I usually monitor Lufthansa and KLM for promotions. It is possible to get a ticket to Singapore for half of the normal price. There are many ferry and air connections from Singapore, Malaysia and Thailand to various places in Indonesia. IMHO flying is cheapest, quickest and most convenient way to travel. Tickets can be bought online. People from many countries can get a visa on arrival for a 25 USD fee on the border.

Sumatra


Last year I spent one month in Bukittinggi learning pencak silat. I wanted to use this opportunity to see and say hello to the teacher and his students and my friends, who helped me to practice last year. Because of that we headed directly to Bukittinggi and did not see the rest of the island.

We arrived to Padang via air from Kuala Lumpur and got visa on arrival. Visa formalities and passport control did not take long.

ATMs are available right in the airport. There is also a money change office, but the rate is very unattractive. I have seen equally low exchange rates on Bali, but never saw anything lower.

Getting from Padang to Bukittinggi is easy. Right in front of the airport building stop local buses. There will be people there trying to get passengers. Tell them you want to get to Bukittinggi. You will get on the local bus to Padang first, and they will take you to the place where you can get on the bus to Bukittinggi.

We were staying in the Orchid Hotel in the Teuku Umar street. I can recommend this place - this is the “standard” place where backpackers stay in Bukittinggi.

One of my friends is a guide in Bukittinggi, so of course we asked him to organize our time there. My friends name is Adrian, his email is adrian_turs@yahoo.co.id. Adrian organized everything for us: sightseeing, transfers, food during the trips. We saw Ngarai Sianok and the flying foxes that sleep there on the trees during the day, Koto Gadang, Minangkabau houses nearby, but the highlight was our one day trip to Harau Valley.

We were sleeping in Echo Valley - a beautiful, charming place - very scenic, very clean. We had another guide in Harau Valley - Conty, sumatra.explore@yahoo.co.id. We spent the whole day walking in the jungle, seeing great views and beautiful waterfalls. We saw a cave going 7 km deep into the mountain, we saw black gibbons, we were swimming under the waterfalls. But the best part was the end of the day, when we were going down the mountain on a wall that I would never believe we can go down if someone would tell me before. It looked like a vertical wall, but Conty led us skillfully on a curvy path where we had to walk on and hold to the roots of trees growing on this wall to get down. I do recommend this tour, it was a great day for us.

Buying things in Bukittinggi is simple. I have the feeling, that in most places the prices are given more or less as they are. I rarely felt the need to haggle in this town - we were usually just paying the asking price; only in some cases, when we were buying more, did we ask for a little discount.

Here are the prices that I have seen on Sumatra in 2009:

Place
Things bought
Price
Bukittinggi
Coca Cola 1.5l
IDR 11500
Bukittinggi
Toast bread - small
IDR 7500
Bukittinggi
Toast bread - big
IDR 14500
Bukittinggi
Tuna in chilli sauce - canned
IDR 16600
Bukittinggi
Restaurant - 2 people food and drinking
IDR 58000
Bukittinggi
Salak 1 kg
IDR 13000
Bukittinggi
Local coffee - 1kg
IDR 40000
Bukittinggi
Cinnamon - 1kg
IDR 20000
Bukittinggi
White pepper - 1kg
IDR 65000
Bukittinggi
Black pepper - 1kg
IDR 60000
Bukittinggi
Nutmeg - 1 kg
IDR 70000
Bukittinggi
Cardamom - 1 kg
IDR 60000
Bukittinggi
3 day sightseeing package around Bukittinggi
USD 200
Bukittinggi
Orange syrup 1,5l
IDR 41300
Bukittinggi
Roasted corncob
IDR 5000
Bukittinggi
Bus from Bukittinggi to Minangkabau airport
IDR 40000
Bukittinggi
Minibus fare within Bukittinggi
IDR 2000
Bukittinggi
Taxi from Bukittinggi to Minangkabau airport
IDR 200000
Bukittinggi
Room in Orchid hotel (for 2 people)
IDR 75000
Bukittinggi
Room for 2 people, TV, warm water (Orchid hotel)
IDR 100000
Bukittinggi
Room for 3 people, TV, warm water (Orchid hotel)
IDR 150000
Bukittinggi
Toilet paper
IDR 3300
Padang
Basic meal for 1 person in the airport restaurant
IDR 30000
Padang
Airport tax for domestic flight
IDR 35000

I do not remember the bus fare from the airport to Padang and from Padang to Bukittinggi, but they were around 20000 - 30000 IDR each.

I did not buy any silver jewellery in Koto Gadang, but heard that other tourists were given prices around 40 EUR. I do think that one can bargain that down, and I really liked the design of brooches, especially the one with the buffalo head. I expected to see even better silver jewellery in Yogyakarta later, but I did not.


Java


We were flying Mandala from Padang to Jakarta. We had a good experience and in the future will want to fly with them again. The flight was delayed for 2 hours, like many other flights operated by Indonesian airlines.

Contrary to the information that I found during my research, there is a DAMRI bus from the airport to the Gambir railway station as late as 23:30. Jalan Jaksa, were the cheap “backpackers hostels” are, is not far from Gambir, but looking for it during the night may be a bit problematic.

We took a bajaj, but had to haggle the fare down to 50% (IDR 10000) of the asking price (IDR 20000).

Generally we did not like the bajajs and that one was the last we used. Taxis in Jakarta are faster, more comfortable, cheaper and save you the tiresome haggling over every couple of cents. I recommend metered taxis for getting around Jakarta.
We did not like the city and left almost immediately changing our schedule a bit. I was disappointed by the lack of bookshops, but I do regret a bit that I did not visit the Pencak Silat centre.

We went to Yogyakarta by train - again contrary to my research our train did not leave from the Gambir railway station, but from Pasar Senen. We took business class, but this does not look even far like something that people in Europe call “business class”. The trip was pretty tiresome, but incomparably more comfortable than my last year bus trip across Sumatra.

West Java looks pretty dirty and crowded, but the eastern part of the Island is nice and beautiful.

Yogyakarta is much more touristy than Sumatra - this was the only place in Indonesia were I met a tout that did not want to understand the word “no”. My first impression of the city was not that good - but once I got outside of Malioboro street I saw a normal Indonesian city and after some time I decided that it was OK. I think that if I had the time to spend there 2 or 3 weeks and learn were the interesting things are, I could like it a lot.

We were staying in Indonesia Hotel, Jln. Sosrowijayan 9, tel. (0274) 587 659. Rooms are basic, staff is very nice and helpful, as everywhere in Indonesia. But I can not recommend this hotel because of cockroaches (we killed at least 2 every day) and rats (ones 3 of them were running on the yard and almost ran into the room).
We bought a tour around eastern Java with drop off in Denpasar from Sosro Tour&Travel (http://www.yogyes.com/en/yogyakarta-tour-operator/sosro-tour/). Generally we are satisfied customers. We changed the buses a little bit more often than one could expect, of course there is no trace of promised aircon in the car, you are rushed from one attraction to another, and sometimes you miss a not-so-attractive attraction because of “technical problems”. But all these things are to be expected, and in principle we received what we expected and are happy.

Borobudur and Prambanan are the prime attractions and you simply have to see them. IMHO Prambanan is nicer, because there are more places to walk around and contemplate, even though the admission to the main temple is closed. I am not that fond of visiting this kind of attractions, but I am happy that I was there and saw both monuments.

I was disappointed by the Prambanan ballet. The open air theatre was great, the story was interesting, the Prambanan in the background of the stage looked very nice. Only the dance was so so...

Both Bromo and Ijen are very nice places and I recommend them both. They are both a bit crowded, but in both places you can go a bit farther than other people around the crater to find the place where you can sit down and enjoy the view.

On the Bromo tour they try to sell you a jeep ride to the view point where you can enjoy the sunrise over Bromo. I do recommend to take the jeep, because this is better than searching for your way to the Bromo during the night, but be warned that this viewpoint at sunrise is a kind of funny place. There are so many people that it is a major challenge to get to the place from where you can actually see the sunrise. The jeep will not bring you to the top, because there will be so many jeeps, that the road will be blocked. Moreover, there will be multiple motorbikes blocking the road for the jeeps to offer the ride to the tourists that are thus forced to climb on feet.

At the viewpoint people climb the fence, trees (I did climb on a tree), trashbins to be able to see the view. Those who did not get to the good place ask those who did to make a photo for them. It all looks very funny.

The view itself is nice, and you can make a nice photo with the view only - no a single person around you ;-)

We were staying in the Sion View Hotel close to Bromo (adress?) and I can recommend it - nice rooms, beautiful view. Nights are cool there, but not that bad. Hotel was included in our tour.

In Yogyakarta, the prices quoted on Malioboro are 2 or 3 times more than the real value, but I did not see anything worth buying there. Outside of Malioboro, things get back to normal.

Place
Things bought
Price
Jakarta
DAMRI bus from CGK airport to Gambir station
IDR 20000
Jakarta
A bottle of mineral water on a filling station
IDR 5000
Jakarta
Metered taxi ride in the city
IDR 30000
Jakarta
Train to Yogyakarta, “bisnis” class
IDR 150000
Jakarta
Second hand handbook of Indonesian
IDR 45000
Jakarta
Cheap Indonesian-English dictionary
IDR 25000
Yogyakarta
Original Indonesian music CD
IDR 400000
Yogyakarta
Transfer to Prambanan
IDR 75000
Yogyakarta
Entry ticket to Prambanan candi
USD 11
Yogyakarta
Entry ticket to Borobudur candi
IDR 120000
Yogyakarta
Entry to Mendut candi
IDR 3700
Yogyakarta
Ramayana ballet, 2-nd “class” (125000 for 1st row)
IDR 100000
Yogyakarta
Ramayana ballet, VIP “class”
IDR 200000
Yogyakarta
Room for 2 with mandi, no hot water
IDR 70000
Yogyakarta
Post stamp for a card to Europe
IDR 8000
Yogyakarta
Airmail package to Europe, 1 kg
IDR 105000
Yogyakarta
Sea mail package to Europe, 3-5 kg
USD 23.57
Yogyakarta
Tour: Bromo + Ijen, drop off in Denpasar (haggling)
IDR 550000
Yogyakarta
Transport to Cemoro Lawang
IDR 150000
Yogyakarta
Transport + room in Cemoro Lawang
IDR 370000
Yogyakarta
Brromo package tour
IDR 460000
Yogyakarta
Tour: Bromo, Ijen, drop off in Ketapang
IDR 670000
Yogyakarta
Tour: Pananjakan, Bromo
IDR 540000
Yogyakarta
Tour: Pananjakan, Bromo, Ijen, Ketapang
IDR 750000
Yogyakarta
Postcard
IDR 3500
???
Jeep to Bromo
IDR 90000
???
Entry ticket to Bromo
IDR 35000
???
Road restaurant, food for 1 person
IDR 30000
???
Watermelon, 1 kg
IDR 2300

Drop off in Ketapang is no problem if you are heading to Denpasar. You just buy a ferry ticket to Bali and are on a ferry in minutes. The bus terminal on the other side is just a few steps from the ferry.

Prices of tours do vary a lot depending on standard and what is included (eg. breakfast, hot shower, aircon, padded chair). The prices above are for services that IMHO are likely to be chosen. This list has a purpose of allowing you to estimate your budget, not to give you precise price list of tourist services.


Bali


My first reaction to Bali was irritation - touts, tourists everywhere (crowded shop and the only Indonesian inside was the shop assistant). We were also unlucky in the first restaurant - slow service (we left without waiting for the last dish), the food not exactly as it should be, but costing 3 times it would on another island. “Art” galleries everywhere, and not much more.

This was the only place where I lost interest towards the local language. But with time I cooled down and can see that if you like shopping for the “art” like masks or similar objects, if you enjoy walking and observing local temples, reflexology massage etc, then Bali is a good place for you.

We stayed in Alamanda Accomodation on Monkey Forest Rd in Ubud (alamanda@balimore.com). I can recommend this place - we killed only one cockroach, the room was spacious; we got the key and could enter or leave the room without disturbing anybody. The walls and plants shielded us from the outside world, the place was cosy, nice and convenient.

Prices on the Ubud market are 5 to 3 times more than the real value - they go immediately down from 5x to 3x in the beginning of haggling. For example, the asking price for a wooden mask was 250000 IDR. I bought one and my friend bought one. In both cases the asking price was the same, and we both paid 50000 in the end. This looked pretty much the same for other articles, too.

Bali so far is the only place in the world, where the sellers tried to change the price after it was agreed - and it happend not once, but 3 times.

Place
Things bought
Price
Ubud
Sprite 1,5L
IDR 15000
Ubud
A packet of some kind of “nuts”
IDR 25000
Ubud
Promotional bottle of Coca Cola 1,5L
IDR 11300
Ubud
A set of small bottle with massage oil
IDR 35000
Ubud
Bicycle rental for 1 day
IDR 25000
Ubud
Smirnoff 0,7 bottle
IDR 72500
Ubud
Sarong on the market (poor quality, asking 150000)
IDR 50000
Ubud
Mineral water 1,5L
IDR 3000
Ubud
Simple food in restaurant, 1 person, drinks incl.
IDR 52000
Ubud
Post stamp for a postcard to Europe
IDR 7500
Ubud
Taxi to the airport, bargain price, normally 200000
IDR 150000
Ubud
Wooden mask on the market
IDR 50000
Ubud
Presentation of local dances
IDR 80000
Ubud
Post parcel to Poland, 20kg, 50x60x140cm
IDR 720000
Denpasar
Airport tax for international flight
IDR 150000

Local tours around Ubud and elsewhere in Bali cost between 200000 and 600000 IDR, depending on the kind of tour (starting with bicycle tours, ending with rafting and similar attractions).



Similar report for Thailand, Malaysia and Singapore will come in the next post.