Paris!

2011 April 6
tags: , ,
by Pedro Kiefer

Depois de três anos, estou de volta a Paris. Dessa vez acompanhado pela Fernanda, minha namorada! Como é bom estar em Paris, dessa vez, o clima está diferente, não é inverno, faz calor, muito calor, as plantas estão verdes, tem flores em todos os canteiros. Mas isso é a parte boa do primeiro trecho da viagem.

Tivemos uma grande surpresa quando chegamos ao aeroporto Salgado Filho! Descobrimos que nossa passagem para o Rio, onde pegaríamos o voo da Air France, não existiam mais! Com sorte, chegamos 1h30 antes do voo. Passamos mais ou menos todo esse tempo no telefone com a agência que vendeu a passagem para resolver a situação. Segundo a funcionária da Gol, que tomou as nossas dores e realmente nos ajudou, a reserva do voo Porto Alegre – Rio tinha deixado de existir no dia 19 de março. Tempo mais que suficiente para a agência nos informar e realocar nosso voo. Mas, como se pode ver, resolvemos todos os problemas a tempo, apesar do enorme stress.

Trocamos de voo para o Rio, e com uma sorte absurda, um voo da Air France das 16h havia sido adiado para às 21h! Trocamos nosso voo das 19h, para o das 21h. Acho que a troca foi benéfica, fez um nerd muito feliz por estar voando pela primeira vez em um 747-400 (um dos meus sonhos desde criança). O voo, como é de se esperar da Air France, foi ótimo! A comida a bordo era boa, serviram Camembert da Président, nhami! Única queixa foi só servirem picolés da Kibon sabor Uva, na caixa térmica da Hägen-Däsz, baita propaganda enganosa – Nada contra a Kibon, mas…

Chegamos em Paris no tempo previsto do novo voo. O terminal novo do Charles de Gaule é bem bacana. Não sei quem é o arquiteto, mas ele usa bastante madeira, pintada com uma cor cimento, de forma que diversas vezes parece que a parede é de concreto. Resultado fica legal, e a acústica do ambiente fica excelente. Depois de fazer a imigração e já com as bagagens, sofremos para descobrir como pegar o RER que passa no aeroporto – que não está funcionando justamente neste final de semana. Quando descobrimos isso, ficamos surpresos com a eficiência da sinalização e estrutura montada para fechar uma estação de metrô por alguns dias. Haviam panfletos, banners, diversos ônibus fazendo a conexão para a estação mais próxima.

Depois de nos instalarmos no hotel, resolvemos fazer um pequeno tour a pé. Caminhamos até o Centre Pompidou, que fica à uns 10min do hotel. Depois seguimos até a Notre Dame, passando pelo Hôtel de Ville. Fomos até a ponta da Île de la Cité, haviam diversas pessoas fazendo piquenique. Uns super sofisticados, com camarões, baguetes, queijos e vinhos, já outros, eram só salgadinhos prontos e cerveja quente. Fizemos uma parada, quase que obrigatória, na Apple Store do Louvre. Aproveitamos para tomar um Caramel Machiato na Starbucks e descansar os pés um pouco. Terminamos a caminhada na L’orangerie.

Centre Pompidou

Centre Pompidou

Notre Dame

Notre Dame

Nanda na ponta da Île de la Cité

Nanda na ponta da Île de la Cité

Agora é hora de jantar!

From CVS to Git to Gitorious!

2011 February 21
by Pedro Kiefer

Migrating from CVS to Git

Last week I’ve offered myself to migrate some ~300 repositories to git. Not an easy task at first, but with the right tools at hand the task becomes manageable. Installing cvs2git, and following its documentation will get you started. In Ubuntu that is as simples as:

sudo apt-get install cvs2svn

I know it’s weird, but cvs2git is bundled in cvs2svn… go figure.

But migrating hundreds of repositories isn’t a task to do manually, so I created a script for automating the process. As I had access to the server files, migrating was easier then I expected. My directory structure was something like:

  • cvs_project_1
    • repo_1
    • repo_2
    • repo_3
  • cvs_project_2

I’ve decided to migrate one project at a time, making it straightforward to verify each repo. My script is the following, bare in mind that it my have some flaws, it worked for me. Test it before erasing your old CVS data.

#!/bin/bash
# Copyright (C) Pedro Kiefer

for f in `cat repo_list`;
do
	FOP=${f/\//\-}
	echo "===== Creating git repository for ${f/\//\-/}/";
	sed -e "s/__REPO__/${f/\//\\/}/g" my-default.options > $FOP.options;
	cvs2git --options=$FOP.options
	rm $FOP.options
	mkdir $FOP.git
	cd $FOP.git
	git init --bare
	cat ../cvs2svn-tmp/git-blob.dat ../cvs2svn-tmp/git-dump.dat | git fast-import
	cd ..
done

The script takes a repo_list file with a list of paths to the CVS repositories. Creating this list is quite easy, something like this should work. Be sure to remove CVSROOT and the root directory.

find cvs_project_1/ -maxdepth 1 -type d | sort > repo_list
vim repo_list

The other file the script need is my-default.options, which is the configuration file used by cvs2git. Most of the default values are good, but you really want to add a list of cvs commiters – so you can map the cvs login to a name + email. The other change need is on the line that sets the repository path. For the script to work you need to have it set as __REPO__. Like this:

run_options.set_project(
    # The filesystem path to the part of the CVS repository (*not* a
    # CVS working copy) that should be converted.  This may be a
    # subdirectory (i.e., a module) within a larger CVS repository.
    r'__REPO__',

That’s it, just run the script, and voilà, git repositories for all your cvs modules.

From Git to Gitorious

The second part of my task was importing all of those git repositories to my local Gitorious install. Again, doing it manually is not the right way to do it. After asking about it on gitorious mailing list and learning some ruby, I’ve created this little script. It creates all the repositories for a given project. The projects were created manually on gitorious, as I had only 6 projects – extending the tool to support the creating of projects should be easy.

After using the script above, I had the following directory structure:

  • project_1/
    • repo_1.git
    • repo_2.git
    • repo_3.git

      The scripts takes as argument the project name, which should be equal to the one you created on gitorious web interface. The script scan the project directory and creates the matching gitorious repositories, copying the data to the newly created repository. Some magic regexp was added to remove version numbers and set uniform names to the new repositories. You might want to edit this to your taste.

      By the way, this is my very first ruby programming, don’t expect it to be pretty!

      #!/usr/bin/env ruby
      # encoding: utf-8
      #--
      # Copyright (C) Pedro Kiefer
      #
      # Mass migrate git repositories to gitorious
      #
      #++
      
      require "/path/to/gitorious/config/environment.rb"
      require "optparse"
      
      def new_repos(opts={})
        Repository.new({
          :name => "foo"
          }.merge(opts))
      end
      
      current_proj = ARGV[0]
      
      @project = Project.find_by_slug(current_proj)
      
      Dir.chdir(current_proj)
      puts Dir.pwd
      files = Dir.glob("*.git")
      
      files.each do |f|
        orig_repo = f
        f = f.gsub(/\.git$/, "")
        f = f.gsub(/_/,"-")
      
        # has version?
        version = f.match(/-([0-9](.[0-9][0-9]*)+)(-)?/)
        f = f.gsub(/-([0-9](.[0-9][0-9]*)+)(-)?/, "")
      
        desc = "Repository for package #{f.downcase}\n"
        desc << "Package version #{version[1]}\n" if version
      
        print "Creating repository for package #{f} ... " 
      
        @repo = new_repos(:name => f.downcase, :project => @project, :owner => @project.owner, :user => @project.user, :description => desc)
        @repo.save
        path = @repo.full_repository_path
        Repository.git_backend.create(path)
        Repository.create_git_repository(@repo.real_gitdir)
        @repo.ready = true
        @repo.save 
      
        FileUtils.cp_r(["#{orig_repo}/branches", "#{orig_repo}/info", "#{orig_repo}/objects", "#{orig_repo}/refs"], @repo.full_repository_path)
        puts "Ok!"
      end
      

Asterisk and FreePBX on Ubuntu Server 10.10

2011 February 14
by Pedro Kiefer

This is just a small gathering of commands and best practices for installing Asterisk and FreePBX on Ubuntu. This worked for me, it has some shortcomings but should work on most of the cases. Feel free to add some comments on better ways of installing it.

The following packages will be installed:

  • Asterisk 1.6.2.7
  • FreePBX 2.8.1

I started with a fresh install of Ubuntu Server 10.10, but if you already have it installed, results should be similar. While installing I selected the LAMP and SSH services, those are pretty basic services which you will need. If you have finished a fresh install, or haven’t updated your system in a while, I suggest running the following lines before continuing with this guide.

sudo apt-get update
sudo apt-get upgrade

Postfix

Although not necessary for running Asterisk and FreePBX, I suggest that you install a MTA agent. If you think this is unnecessary on your setup skip to the next section. Postfix is my MTA of choice, so we are going to install it. When prompt about which configuration should be done to it, select Internet with smarthost, just confirm the other options.

sudo apt-get install postfix

Okey, postfix installed, time to edit the basic configuration, add or change the following lines to /etc/postfix/main.cf:

relayhost = [smtp.gmail.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_CAfile = /etc/postfix/cacert.pem
smtp_use_tls = yes

The password for accessing your external relay must be saved to
/etc/postfix/sasl_passwd, add the following this file:

[smtp.gmail.com]:587    user.name@gmail.com:password

Fix the permissions on this file:

sudo chmod 400 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd

Add the appropriate ca-certificate to /etc/postfix/cacert.pem. For gmail, that’s Thawte Consulting, so add their ca-certificate.

cat /etc/ssl/certs/Thawte_Premium_Server_CA.pem | sudo tee -a /etc/postfix/cacert.pem

Restart postfix:

sudo /etc/init.d/postfix restart

Avoid sending mail as root

Edit /etc/aliases, add the following:

root: server@domain.tld

Run the new alias command:

newaliases

Create a /etc/postfix/sender_canonical file mapping user -> email such as:

root            server@domain.tld

Run the following lines:

sudo postmap hash:/etc/postfix/sender_canonical

Add the following line to /etc/postfix/main.cf:

sender_canonical_maps=hash:/etc/postfix/sender_canonical

Restart postfix:

sudo /etc/init.d/postfix restart

PHP

When you selected the LAMP service on your Ubuntu install, you automatically got PHP5 installed. Now you just have to install some additional packages that didn’t get installed. So run the following line to install them.

sudo apt-get install php5-gd php-pear php-db sox curl

phpMyAdmin

One might find useful to have phpMyAdmin installed for managing the MySQL database used by FrePBX and Asterisk. If you don’t know what phpMyAdmin is, you can skip to the next section.

Asterisk

Ubuntu 10.10 provides pre-compiled asterisk packages, using that is way more easier than backing your own asterisk. Run the following to install it, and all of its dependencies.

sudo apt-get install asterisk asterisk-mysql asterisk-mp3 asterisk-sounds-extra

Dahdi

This is a really short how-to for configuring Dahdi, it just covers the bare minimum, but it works ok. First of all, load the necessary kernel modules, in my case for a TDM400P it was the following line:

sudo modprobe wctdm

You might wanna check if the module was loaded and configured your hardware properly, so run a dmesg. If everything is alright, you have to create the dadhi configuration file. That’s really easy, just run:

sudo dahdi_genconf -vvvv

Warning: Be careful when you run this on a production system, it will override the current dahdi configuration file.

Edit /etc/dahdi/system.conf and set the correct loadzone and defaultzone for your country code. I like to use vim to edit configuration files, but you can use any text editor.

sudo vim /etc/dahdi/system.conf

Now check if channels are up an running, run dahdi_cfg:

sudo dahdi_cfg -vvv

Next you have to edit /etc/asterisk/chan_dahdi.conf to configure the channels, this is what asterisk will see and use to send and receive calls.

Apache

Before running the install command, you have to configure your apache server. I prefer to use virtual host, and as of lately I have adopted the following layout for my server:

  • /var/www/address/conf
  • /var/www/address/public
  • /var/www/address/log

In the conf I store the necessary vhost configuration, in public lives the public accessible files, and log hosts the logging files. Feel free to use your own personal taste on installing webapps. For those who want to stick with the how-to, create the needed directories:

sudo mkdir /var/www/pabx.domain/
sudo mkdir /var/www/pabx.domain/conf
sudo mkdir /var/www/pabx.domain/log
sudo mkdir /var/www/pabx.domain/public

Now create a /var/www/pabx.domain/conf/vhost.conf file:

sudo vim /var/www/pabx.domain/conf/vhost.conf

And paste the following lines, change it accordingly to your domain.

<VirtualHost *:80>
   ServerName pabx.domain
   ServerAlias pabx.domain

   ServerAdmin admin@domain.tld
   ErrorLog /var/www/pabx.domain/log/error.log
   CustomLog /var/www/pabx.domain/log/access.log combined

   DocumentRoot /var/www/pabx.domain/public
   <Directory /var/www/pabx.domain/public>
       Options Indexes FollowSymLinks MultiViews
       Order allow,deny
       AllowOverride All
       Allow from all
   </Directory>

   <Directory /var/www/pabx.domain/public/admin>
       AuthType Basic
       AuthName "Restricted Area"
       AuthUserFile freepbx-passwd
       Require user admin
   </Directory>
</VirtualHost>

With the file created, add the vhost to the sites-enabled directory, with:

sudo ln -s /var/www/pabx.domain/conf/vhost.conf /etc/apache2/sites-available/pabx.domain
cd /etc/apache2/sites-enabled/
sudo ln -s ../sites-available/pabx.domain

For now, create an htpasswd file to protect the access to freepbx.

sudo htpasswd -c /etc/apache2/freepbx-passwd admin

And finally, restart apache.

sudo /etc/init.d/apache2 restart

FreePBX

Your Asterisk install should be working by now, so it’s time to install a nice web user interface. Ubuntu doesn’t provide a package for FreePBX, so grab the latest stable source code from FreePBX site.

cd /tmp
wget http://mirror.freepbx.org/freepbx-2.8.1.tar.gz
cd /usr/src
sudo tar xvzf /tmp/freepbx-2.8.1.tar.gz
cd freepbx-2.8.1/

You can equally extract the tarball on your home directory. It doesn’t make any difference. Now it’s time to create the database, the user used to access it, and populate the basic tables. This will create and import the basic tables to asterisk and asterisk cdr database, run this from the recently extracted directory.

mysqladmin create asterisk -u root -p
mysqladmin create asteriskcdrdb -u root -p
mysql -u root -p asterisk < SQL/newinstall.sql
mysql -u root -p asteriskcdrdb < SQL/cdr_mysql_table.sql

With the tables in-place, it's time to create the user with privileges to access and edit those tables. Open a mysql prompt with:

mysql -u root -p

On the prompt run the following queries:

GRANT ALL PRIVILEGES ON asterisk.* TO asterisk@localhost IDENTIFIED BY 'badasspassword';
GRANT ALL PRIVILEGES ON asteriskcdrdb.* TO  asterisk@localhost IDENTIFIED BY 'badasspassword';
flush privileges;
quit;

Don't forget to change the password!

Before running the install command, make a copy of /etc/asterisk/modules.conf. FreePBX rewrites the file and trashes Asterisk installation. If you restart Asterisk after installing FreePBX Asterisk dies with no message.

sudo cp /etc/asterisk/modules.conf ~/asterisk-modules.conf

Ok, we are ready to install freepbx to /var/www/pabx.domain/public:

sudo ./install_amp

The install script will ask for some configuration data, eg. were to install freepbx (/var/www/pabx.domain/public), sql password, asterisk password, etc. Take note of the passwords you used, you might need them later.

The output from the install script is somewhat like this:

...
Enter your USERNAME to connect to the 'asterisk' database:
 [asteriskuser] asterisk
Enter your PASSWORD to connect to the 'asterisk' database:
 [amp109] badasspassword
Enter the hostname of the 'asterisk' database:
 [localhost]
Enter a USERNAME to connect to the Asterisk Manager interface:
 [admin]
Enter a PASSWORD to connect to the Asterisk Manager interface:
 [amp111]
Enter the path to use for your AMP web root:
 [/var/www/html]
/var/www/pabx.domain/public
Enter the IP ADDRESS or hostname used to access the AMP web-admin:
 [xx.xx.xx.xx] pabx.domain
Enter a PASSWORD to perform call transfers with the Flash Operator Panel:
 [passw0rd] password
Use simple Extensions [extensions] admin or separate Devices and Users [deviceanduser]?
 [extensions]
Enter directory in which to store AMP executable scripts:
 [/var/lib/asterisk/bin]
...

Restore asterisk-modules.conf file, which you backed up before installing FreePBX:

sudo cp ~/asterisk-modules.conf /etc/asterisk/modules.conf

Apache runs as www-data, Asterisk as user asterisk, so we have to change some permission to make both programs work together. First, add www-data to asterisk group:

sudo adduser www-data asterisk

Fix the permissions from amportal, add these lines to the end of /etc/amportal.conf:

AMPASTERISKUSER=www-data
AMPASTERISKGROUP=asterisk
AMPASTERISKWEBUSER=www-data
AMPASTERISKWEBGROUP=asterisk

Everything in place, time to start amportal:

sudo amportal start

Open your web browser and go to http://pabx.domain/ and you will be greeted with FreePBX site. I strongly suggest you to upgrade and install the FreePBX modules you will need, so go to Modules Admin and click on Check for online updates.

Start asterisk with amportal

Before we finish, lets make amportal script to manage asterisk and run it through the safe_asterisk script, for that, we have to remove asterisk from rc.d:

sudo update-rc.d -f asterisk remove

Now edit safe_asterisk, to make sure it runs on background, edit the variable BACKGROUND to zero:

sudo sed -e s/BACKGROUND=0/BACKGROUND=1/ -i /usr/sbin/safe_asterisk

We have to start amportal after booting, so call amportal start in /etc/rc.local. Edit your /etc/rc.local and add the following line before the exit 0 line.

/usr/local/sbin/amportal start

Reboot your machine, and check that everything is still working. Have fun!

Ponteiros!

2010 July 4
by Pedro Kiefer

Outro dia no trabalho resolvemos brincar com ponteiros, não que o código que resultou da brincadeira seja algo bom, mas foi divertido. Basicamente tinhamos uma função que traduzia uma string interna passada para a string que utilizariamos em tela, software foi crescendo e mais traduções eram necessárias para a mesma string.

Então aos exemplos, do que tinhamos, da brincadeira que fizemos com os ponteiros e o que seria a melhor solução para o código. Antes algumas definições, a estrutura que contém as strings é a seguinte, basicamente um mapa 1 pra n:

enum {
	KERNEL = 1,
	WEB,
	CLI
};

struct test {
	int idx;
	char *kernel;
	char *web;
	char *cli;
};

struct test example[] = {
	{.idx = 0, .kernel = "eth0", .web = "Ethernet 0", .cli = "ethernet0" },
	{.idx = 1, .kernel = "eth1", .web = "Ethernet 1", .cli = "ethernet1" },
	{.idx = 2, .kernel = "eth2", .web = "Ethernet 2", .cli = "ethernet2" },
	{.idx = 3, .kernel = NULL, .web = NULL, .cli = NULL}
};

A versão original da função que procurava pela string correta era algo parecido com:

char *get_str_for_v1(const char *eth, enum str_type type)
{
	char *rtn = NULL;
	int i;

	switch (type) {
	case KERNEL:
		for (i = 0; i < sizeof (example); i++) {
			if (strcmp(eth, example[i].kernel) == 0) {
				rtn = example[i].kernel;
				break;
			}
		}
		break;
	case WEB:
		for (i = 0; i < sizeof (example); i++) {
			if (strcmp(eth, example[i].kernel) == 0) {
				rtn = example[i].web;
				break;
			}
		}
		break;
	case CLI:
		for (i = 0; i < sizeof (example); i++) {
			if (strcmp(eth, example[i].kernel) == 0) {
				rtn = example[i].cli;
				break;
			}
		}
		break;
	}

	return rtn;
}

O que é obviamente muito feio! Com excesso de código repetido, péssimo de ler e dar manutenção. Vendo esse código propus o desafio de escrever a mesma função com um for só. Meu colega de trabalho resolveu usar ponteiros pra resolver isso, calculando o offset de cada um dos membros da estrutura e recuperando o ponteiro da string a partir do offset. Claro, isso parece muito simples de se fazer… mas na hora de escrever o código, demorou bem mais do que imaginavamos, mas foi bastante divertido entender como funcionam os casts em C.

O resultado foi o seguinte código, as macros eu adicionei para facilitar a leitura do código, mais sobre elas daqui um pouco…

#define member_offset(type, member) \
			((unsigned long)(&((type *)0)->member))
#define get_member_by_offset(ptr, type, offset) \
			((type *)((void *)(ptr) + offset))

char *get_str_for_pointer_fun(const char *eth, enum str_type type)
{
	int i;
	unsigned long offset;

	switch(type) {
	case KERNEL:
		offset = member_offset(struct test, kernel);
		break;
	case WEB:
		offset = member_offset(struct test, web);
		break;
	case CLI:
		offset = member_offset(struct test, cli);
		break;
	}

	for (i = 0; i < sizeof (example); i++) {
		if (strcmp(eth, example[i].kernel) == 0) {
			return *get_member_by_offset(&example[i], char *, offset);
		}
	}

	return NULL;
}

Com certeza não é a melhor implementação possível, talvez seja a mais complexa de se entender, especialmente se não for utilizada uma macro para esconder a implementação. Mas é uma implementação divertida, ou pelo menos foi divertido tentar achar a combinação correta de ponteiros para gerá-la.

E finalmente o código que eu julgo o mais aceitável:

char *get_str_for_v2(const char *eth, enum str_type type)
{
	int i;

	for (i = 0; i < sizeof (example); i++) {
		if (strcmp(eth, example[i].kernel) == 0) {
			switch (type) {
			case KERNEL:
				return example[i].kernel;
			case WEB:
				return example[i].web;
			case CLI:
				return example[i].cli;
			}
		}
	}

	return NULL;
}

Macros

Bom, disse que ia explicar o funcionamento das duas macros utilizadas, então recapitulando elas:

#define member_offset(type, member) \
			((unsigned long)(&((type *)0)->member))

#define get_member_by_offset(ptr, type, offset) \
			((type *)((void *)(ptr) + offset))

A macro member_offset calcula o offset de um membro qualquer de um estrutura. Para fazer isso, ela dá um cast para um ponteiro do tipo da estrutura requerida ao valor 0. O que parece estranho a princípio, mas faz todo o sentido! Do ponteiro para zero até o membro requerido da estrutura existem os n bytes que representam o offset em memória do início da estrutura até o membro.

Munido do valor do offset, podemos então recuperar a estrutura que se encontra naquele offset utilizando a macro get_member_by_offset. Obviamente é necessário sabermos qual o tipo de dado que se encontra naquele offset! Então a partir do ponteiro (endereço) da estrura que queremos acessar podemos somar o offset do membro requerido. Mas só somar o offset não resolve, é necessário dar os casts apropriados.

Como queremos um que o endereço calculado (ponteiro original + offset) seja um ponteiro, é necessário indicar isto ao compilador, colocando um * no cast do cálculo. Além disso, é necessário indicar que tipo de ponteiro estamos recuperando naquele endereço, portanto o cast vira (type *). No exemplo, o cast é (char **) indicando corretamente que naquele endereço encontra-se uma string.

Esse mesmo método de recuperar um membro de uma estrutura é utilizado na implementação de listas no kernel do linux. A macro definida é um pouco diferente, uma vez que ela realiza estas duas operações de uma só vez, mas a explicação do seu funcionamento é a mesma! Segue a sua implementação:

#define list_entry(ptr, type, member) \
	((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

Basicamente o que este código faz é recuperar o ponteiro para a estrutura a partir de um membro qualquer dela. No caso das listas, o membro utilizado para recuperar a estrutura original é o struct list_head. Com isso, é possível utilizar diversos struct list_head numa mesma estrutura, adicionando ou não a estrutura alocada a uma das listas.

Fundação Iberê

2010 June 20
by Pedro Kiefer

Outro dia me dei conta de que nunca tinha tirado uma foto da Fundação Iberê, apesar de ter acompanhado a obra de perto – vendo meu pai organizar o livro sobre o projeto e construção; indo à obra com o Fábio, que fotografou todo o processo da construção. Então resolvi que na minha próxima ida iria tirar algumas fotos.

Também aproveitei a ida pra ver a exposição da Mira Schendel e do León Ferrari, que realmente vale a pena. Saí de lá com a vontade de ter um trabalho de letraset da Mira, simplesmente genial!

Bom, o resultado das fotos está ai abaixo… não ficaram o supra-sumo, e também faltou uma foto do paredão de concreto dos fundos que é muito legal! Mas na próxima ida eu tiro essa foto!

Fundação Iberê Camargo

Fundação Iberê Camargo

Bad Behavior has blocked 290 access attempts in the last 7 days.